Import Cobalt 23.lts.2.309559
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 08dba15..12bbd12 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -168,7 +168,7 @@
         types: [text]
         # TODO: Remove docker-compose-windows.yml after internal check evaluates
         # properly on it.
-        exclude: '(^docker-compose-windows.yml|EXCLUDE\.FILES(\.RECURSIVE)?|codereview\.settings)$'
+        exclude: '(^docker-compose-windows(-internal)?.yml|EXCLUDE\.FILES(\.RECURSIVE)?|codereview\.settings)$'
     -   id: gn-format
         name: GN format
         entry: gn format
diff --git a/base/single_thread_task_runner.cc b/base/single_thread_task_runner.cc
index 755a0fc..d4baf23 100644
--- a/base/single_thread_task_runner.cc
+++ b/base/single_thread_task_runner.cc
@@ -40,11 +40,15 @@
   base::WaitableEvent task_finished(
       base::WaitableEvent::ResetPolicy::MANUAL,
       base::WaitableEvent::InitialState::NOT_SIGNALED);
-  PostTask(from_here,
+  bool task_may_run = PostTask(from_here,
            base::Bind(&RunAndSignal, task, base::Unretained(&task_finished)));
+  DCHECK(task_may_run)
+      << "Task that will never run posted with PostBlockingTask.";
 
-  // Wait for the task to complete before proceeding.
-  task_finished.Wait();
+  if (task_may_run) {
+    // Wait for the task to complete before proceeding.
+    task_finished.Wait();
+  }
 }
 #endif
 }  // namespace base
diff --git a/base/values.cc b/base/values.cc
index 49440e7..ade6b6f 100644
--- a/base/values.cc
+++ b/base/values.cc
@@ -272,6 +272,11 @@
   return list_;
 }
 
+Value::ListStorage Value::TakeList() {
+  CHECK(is_list());
+  return std::exchange(list_, {});
+}
+
 Value* Value::FindKey(StringPiece key) {
   return const_cast<Value*>(static_cast<const Value*>(this)->FindKey(key));
 }
diff --git a/base/values.h b/base/values.h
index c9964a8..0f9558d 100644
--- a/base/values.h
+++ b/base/values.h
@@ -178,6 +178,11 @@
   ListStorage& GetList();
   const ListStorage& GetList() const;
 
+  // Transfers ownership of the underlying list to the caller. Subsequent
+  // calls to `GetList()` will return an empty list.
+  // Note: This requires that `type()` is Type::LIST.
+  ListStorage TakeList();
+
   // |FindKey| looks up |key| in the underlying dictionary. If found, it returns
   // a pointer to the element. Otherwise it returns nullptr.
   // returned. Callers are expected to perform a check against null before using
diff --git a/cobalt/bindings/templates/dictionary.h.template b/cobalt/bindings/templates/dictionary.h.template
index b31fb2c..d178bd6 100644
--- a/cobalt/bindings/templates/dictionary.h.template
+++ b/cobalt/bindings/templates/dictionary.h.template
@@ -60,7 +60,7 @@
 {% if used_class.conditional %}
 #if defined({{used_class.conditional}})
 {% endif %}
-using {{used_class.fully_qualified_name}};
+using ::{{used_class.fully_qualified_name}};
 {% if used_class.conditional %}
 #endif  // defined({{used_class.conditional}})
 {% endif %}
diff --git a/cobalt/black_box_tests/black_box_tests.py b/cobalt/black_box_tests/black_box_tests.py
index 737c875..34b2ffe 100644
--- a/cobalt/black_box_tests/black_box_tests.py
+++ b/cobalt/black_box_tests/black_box_tests.py
@@ -70,6 +70,8 @@
     'web_debugger',
     'web_platform_tests',
     'web_worker_test',
+    'worker_csp_test',
+    'service_worker_message_test',
     'service_worker_test',
 ]
 # These tests can only be run on platforms whose app launcher can send deep
diff --git a/cobalt/black_box_tests/testdata/black_box_js_test_utils.js b/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
index ed1d883..d488a55 100644
--- a/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
+++ b/cobalt/black_box_tests/testdata/black_box_js_test_utils.js
@@ -10,6 +10,10 @@
 const EFFECT_AFTER_VISIBILITY_CHANGE_TIMEOUT_SECONDS = 5;
 
 function notReached() {
+  // Show the stack that triggered this function.
+  try { throw Error('') } catch (error_object) {
+    console.log(`${error_object.stack}`);
+ }
   document.body.setAttribute(TEST_STATUS_ELEMENT_NAME, FAILURE_MESSAGE);
 }
 
diff --git a/cobalt/black_box_tests/testdata/http_cache.html b/cobalt/black_box_tests/testdata/http_cache.html
index cad147f..e1f2e48 100644
--- a/cobalt/black_box_tests/testdata/http_cache.html
+++ b/cobalt/black_box_tests/testdata/http_cache.html
@@ -1,10 +1,34 @@
 <!DOCTYPE html>
 
+<!--
+  To add a new check to this test:
+    1. Create the test file to be loaded and place it in
+       /testdata/http_cache_test_resources/. The server delays responses when
+       serving files from that directory, to make a clear distinction of what's
+       being loaded from the cache.
+    2. Add the file name to either CACHED_ELEMENT_LOCATIONS if you expect that
+       file's mime type to be cached or UNCACHED_ELEMENT_LOCATIONS if you
+       expect the file to be excluded from the cache.
+    3. Add a call to the window.onload function to fetch your file, making a
+       call to measureLoadTime after the file has been loaded.
+-->
 <head>
   <title>Cobalt HTTP Cache Test</title>
   <script src="black_box_js_test_utils.js"></script>
   <script>
-    var NUMBER_OF_CACHE_ELEMENTS = Number.MAX_SAFE_INTEGER;
+    var TESTDATA_LOCATION = window.location.protocol + '//'
+                              + window.location.host
+                              + '/testdata/http_cache_test_resources/';
+    var CACHED_ELEMENT_LOCATIONS = {
+      'js': TESTDATA_LOCATION + 'http_cache.js',
+      'img': TESTDATA_LOCATION + 'cobalt_logo.png',
+      'css': TESTDATA_LOCATION + 'http_cache.css',
+    };
+    var UNCACHED_ELEMENT_LOCATIONS = {
+      'txt': TESTDATA_LOCATION + 'http_cache.txt',
+    };
+    var EXPECTED_NUMBER_OF_ELEMENTS = Object.keys(CACHED_ELEMENT_LOCATIONS).length
+                                      + Object.keys(UNCACHED_ELEMENT_LOCATIONS).length;
     var loadTimes = new Map();
     var cacheSize = 0;
 
@@ -20,50 +44,90 @@
     var initialLoadTime = loadTimes.has('initialLoadTime') ?
       loadTimes.get('initialLoadTime') : new Date().getTime();
     var reloadUrl = 'http_cache.html?initialLoadTime=' + initialLoadTime;
+    // Append the timestamp string to each resource url to force reload the
+    // files the first time the page is accessed each test run. The load time
+    // is recorded on the first run and passed to the second run to ensure
+    // the same url is used both times.
+    var timestampString = '?t=' + initialLoadTime;
 
+    // Validate the tranferSize attribute of each performance entry specified
+    // in CACHED_ELEMENT_LOCATIONS and UNCACHED_ELEMENT_LOCATIONS.
+    function checkTransferSizes() {
+      let elementsChecked = 0;
+      // https://www.w3.org/TR/resource-timing-2/#dom-performanceresourcetiming-transfersize
+      // Verify that the transferSize attribute for each resource is less than
+      // 300. If greater than 300, it wasn't fetched from the cache.
+      for (let location in CACHED_ELEMENT_LOCATIONS) {
+        elementsChecked++;
+        let tranferSize = performance.getEntriesByName(CACHED_ELEMENT_LOCATIONS[location] + timestampString)[0].transferSize;
+        if (tranferSize > 300) {
+          console.log('Element at ' + CACHED_ELEMENT_LOCATIONS[location]
+                        + ' was expected to be cached, but was loaded from the server instead.');
+          notReached();
+        }
+      }
+
+      for (let location in UNCACHED_ELEMENT_LOCATIONS) {
+        elementsChecked++;
+        let tranferSize = performance.getEntriesByName(UNCACHED_ELEMENT_LOCATIONS[location] + timestampString)[0].transferSize;
+        if (tranferSize <= 300) {
+          console.log('Element at ' + UNCACHED_ELEMENT_LOCATIONS[location]
+                        + ' was not expected to be cached, but was loaded from the cache.');
+          notReached();
+        }
+      }
+
+      if (elementsChecked == (EXPECTED_NUMBER_OF_ELEMENTS)) {
+        onEndTest();
+      } else {
+        console.log(elementsChecked + ' elements were checked, but '
+                      + EXPECTED_NUMBER_OF_ELEMENTS
+                      + ' elements were expected. Verify that EXPECTED_NUMBER_OF_ELEMENTS is correctly set.');
+        notReached();
+      }
+    }
+
+    // On first page load, measure the load time of the specified element and
+    // update the reloadUrl to record the load time. Once the expected number of
+    // elements is reached, reload the page.
+    // On second load, measure the load time of the specified element and
+    // compare it to the first load time to validate if the cache was used,
+    // failing the test if the second load took longer.
     function measureLoadTime(element, initialTime) {
       cacheSize++;
       onLoadTime = new Date().getTime() - initialTime;
       if (loadTimes.has(element)) {
-        // TODO: change to using transferSize once its implemented (b/231475964)
-        // Load times might give false positives, but should rarely give false
-        // negatives (i.e. this may pass when caching didn't actually occur,
-        // but will likely never fail when caching does occur).
         console.log(element + ' first load time: ' + loadTimes.get(element));
         console.log(element + ' second load time: ' + onLoadTime);
-        assertTrue(onLoadTime <= loadTimes.get(element));
-        if (cacheSize == NUMBER_OF_CACHE_ELEMENTS) {
-          onEndTest();
+        if (cacheSize == EXPECTED_NUMBER_OF_ELEMENTS) {
+          setTimeout(() => { checkTransferSizes(); }, 500);
         }
       } else {
         reloadUrl += '&' + element + '=' + onLoadTime;
-        if (cacheSize == NUMBER_OF_CACHE_ELEMENTS) {
+        if (cacheSize == EXPECTED_NUMBER_OF_ELEMENTS) {
           setTimeout(() => { window.location.href = reloadUrl; }, 100);
         }
       }
     }
 
+
+
     // Add elements after window loads. In Cobalt, adding the onload property
     // directly to an html tag doesn't execute.
     window.onload = function () {
-      NUMBER_OF_CACHE_ELEMENTS = document.getElementsByTagName('div').length;
-      // Append the timestamp string to each resource url to force reload the
-      // files the first time the page is accessed each test run. The load time
-      // is recorded on the first run and passed to the second run to ensure
-      // the same url is used both times.
-      let timestampString = '?t=' + initialLoadTime;
-
+      // text/javascript
       let initialJsTime = new Date().getTime();
       let script = document.createElement('script');
-      script.onload = () => {
+      script.onload = (event) => {
         measureLoadTime('javascript', initialJsTime);
       };
       script.onerror = () => {
         notReached();
       };
-      script.src = 'http_cache_test_resources/http_cache.js' + timestampString;
+      script.src = CACHED_ELEMENT_LOCATIONS['js'] + timestampString;
       document.getElementById('js_div').appendChild(script);
 
+      // image/png
       let initialImgTime = new Date().getTime();
       let image = document.createElement('img');
       image.onload = () => {
@@ -72,10 +136,11 @@
       image.onerror = (e) => {
         notReached();
       };
-      image.src = 'http_cache_test_resources/cobalt_logo.png' + timestampString;
+      image.src = CACHED_ELEMENT_LOCATIONS['img'] + timestampString;
       image.alt = 'falied to load image';
       document.getElementById('image_div').appendChild(image);
 
+      // text/css
       let initialCssTime = new Date().getTime();
       let css = document.createElement('link');
       css.onload = () => {
@@ -85,8 +150,19 @@
         notReached();
       };
       css.rel = 'stylesheet';
-      css.href = 'http_cache_test_resources/http_cache.css' + timestampString;
+      css.href = CACHED_ELEMENT_LOCATIONS['css'] + timestampString;
       document.getElementById('css_div').appendChild(css);
+
+      // text/plain
+      let initialTxtTime = new Date().getTime();
+      let txt = document.createElement('p');
+      fetch(UNCACHED_ELEMENT_LOCATIONS['txt'] + timestampString)
+        .then(response => response.text())
+        .then(text => {
+          txt.innerHTML = text;
+          document.getElementById('txt_div').appendChild(txt);
+          measureLoadTime('txt', initialTxtTime);
+      });
     }
 
   </script>
@@ -103,4 +179,7 @@
   <div id="css_div">
     <h2>Loading CSS file</h2>
   </div>
+  <div id="txt_div">
+    <h2>Loading txt file</h2>
+  </div>
 </body>
diff --git a/cobalt/black_box_tests/testdata/http_cache_test_resources/http_cache.txt b/cobalt/black_box_tests/testdata/http_cache_test_resources/http_cache.txt
new file mode 100644
index 0000000..ad4f0dc
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/http_cache_test_resources/http_cache.txt
@@ -0,0 +1 @@
+Cobalt does not cache text/plain files. This is NOT expected to be cached.
diff --git a/cobalt/black_box_tests/testdata/service_worker_message_test.html b/cobalt/black_box_tests/testdata/service_worker_message_test.html
new file mode 100644
index 0000000..8b74c05
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_message_test.html
@@ -0,0 +1,153 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!--
+  This is a basic test of the message event for service workers.
+-->
+
+<html>
+
+<head>
+    <title>Cobalt Service Worker Message Test</title>
+    <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body>
+    <script>
+        var expected_event_count = 0;
+
+        function count_event(expected_value) {
+            expected_event_count += 1;
+            console.log('Expected events counted:', expected_event_count)
+            if (expected_value) {
+                assertEqual(expected_value, expected_event_count)
+            }
+            return expected_event_count;
+        }
+
+        navigator.serviceWorker.onmessage = function (event) {
+            console.log('Got onmessage event ', JSON.stringify(event.data));;
+            assertTrue(event instanceof MessageEvent);
+            assertEqual('[object MessageEvent]', `${event}`);
+            assertEqual('[object ServiceWorkerContainer]', `${event.target}`);
+            assertEqual('[object Object]', `${event.data}`);
+            switch (count_event()) {
+                case 3:
+                    assertEqual('Foo', event.data.text);
+                    assertEqual('BarString', event.data.bar);
+                    break;
+                case 4:
+                    assertEqual('Service Worker received a message from source',
+                        event.data.text);
+                    assertEqual('{"message":"Registration ready resolved."}',
+                        JSON.stringify(event.data.data));
+                    assertEqual('[object ExtendableMessageEvent]',
+                        event.data.event_type);
+                    assertEqual('visible', event.data.visibilityState);
+                    assertEqual(true, event.data.focused);
+                    break;
+                case 5:
+                    assertEqual('Service Worker received a message from client',
+                        event.data.text);
+                    assertEqual('{"message":"Registration ready resolved."}',
+                        JSON.stringify(event.data.data));
+                    assertEqual('[object ExtendableMessageEvent]',
+                        event.data.event_type);
+                    assertEqual('visible', event.data.visibilityState);
+                    assertEqual(true, event.data.focused);
+                    break;
+                case 6:
+                    assertEqual('Foo', event.data.text);
+                    assertEqual('BarString', event.data.bar);
+                    break;
+                case 7:
+                    assertEqual('Service Worker received a message from source',
+                        event.data.text);
+                    assertEqual('"Controllerchange event received."',
+                        JSON.stringify(event.data.data));
+                    assertEqual('[object ExtendableMessageEvent]',
+                        event.data.event_type);
+                    assertEqual('visible', event.data.visibilityState);
+                    assertEqual(true, event.data.focused);
+                    break;
+                case 8:
+                    assertEqual('Service Worker received a message from client',
+                        event.data.text);
+                    assertEqual('"Controllerchange event received."',
+                        JSON.stringify(event.data.data));
+                    assertEqual('[object ExtendableMessageEvent]',
+                        event.data.event_type);
+                    assertEqual('visible', event.data.visibilityState);
+                    assertEqual(true, event.data.focused);
+                    break;
+                default:
+                    notReached();
+                    break;
+            }
+
+        }
+
+        navigator.serviceWorker.oncontrollerchange = function (event) {
+            console.log('Got oncontrollerchange event', event.target);
+            count_event(2);
+            navigator.serviceWorker.controller.postMessage(
+                'Controllerchange event received.');
+        }
+
+        navigator.serviceWorker.ready.then(function (
+            registration) {
+            assertNotEqual(null, registration);
+            assertNotEqual(null, registration.active);
+            registration.active.postMessage({ message:
+                'Registration ready resolved.'});
+            count_event(1);
+            window.setTimeout(
+                () => {
+                    registration.unregister()
+                        .then(function (success) {
+                            console.log('(Expected) unregister success :',
+                                success);
+                            count_event(9);
+                        }, function (error) {
+                            console.log('(Unexpected) unregister ' +
+                                `${error}`, error);
+                            assertIncludes('SecurityError: ', `${error}`);
+                            notReached();
+                        });
+                }, 1000);
+        });
+
+        navigator.serviceWorker.register('service_worker_message_test.js', {
+            scope: './',
+        }).catch(function (error) {
+            console.log('(Unexpected) :', error);
+            notReached();
+        });
+
+        setupFinished();
+        // This delay has to be long enough to guarantee that the test has
+        // finished.
+        window.setTimeout(
+            () => {
+                console.log('Events:', expected_event_count)
+                assertEqual(9, expected_event_count);
+                onEndTest();
+            }, 2000);
+
+    </script>
+</body>
+
+</html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_message_test.js b/cobalt/black_box_tests/testdata/service_worker_message_test.js
new file mode 100644
index 0000000..956924a
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_message_test.js
@@ -0,0 +1,60 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+console.log('Service Worker Script Started');
+self.onmessage = function (event) {
+  console.log('Got onmessage event', event, JSON.stringify(event.data), event.source, event.target);
+  if (event instanceof ExtendableMessageEvent) {
+    var options = {
+      includeUncontrolled: false, type: 'window'
+    };
+    event.waitUntil(self.clients.matchAll(options).then(function (clients) {
+      for (var i = 0; i < clients.length; i++) {
+        message = { text: 'Foo', bar: 'BarString' };
+        console.log('Posting to client:', JSON.stringify(message));
+        clients[i].postMessage(message);
+      }
+    }));
+    event.waitUntil(self.clients.matchAll(options).then(function (clients) {
+      message = {
+        text: 'Service Worker received a message from source',
+        data: event.data,
+        visibilityState: event.source.visibilityState,
+        focused: event.source.focused,
+        event_type: `${event}`
+      }
+      console.log('Posting to client:', JSON.stringify(message));
+      event.source.postMessage(message);
+      for (var i = 0; i < clients.length; i++) {
+        message = {
+          text: 'Service Worker received a message from client',
+          data: event.data,
+          visibilityState: clients[i].visibilityState,
+          focused: clients[i].focused,
+          event_type: `${event}`
+        }
+        console.log('Posting to client:', JSON.stringify(message));
+        clients[i].postMessage(message);
+      }
+    }));
+  }
+}
+
+self.onactivate = function (e) {
+  e.waitUntil(self.clients.claim().then(function (result) {
+    console.log('(Expected) self.clients.claim():', result);
+  }, function (error) {
+    console.log(`(Unexpected) self.clients.claim(): ${error}`, error);
+  }));
+}
diff --git a/cobalt/black_box_tests/testdata/service_worker_test.html b/cobalt/black_box_tests/testdata/service_worker_test.html
index 7450faf..34782ec 100644
--- a/cobalt/black_box_tests/testdata/service_worker_test.html
+++ b/cobalt/black_box_tests/testdata/service_worker_test.html
@@ -27,401 +27,7 @@
 </head>
 
 <body>
-    <script>
-        var expected_event_count = 0;
-
-        function count_event(expected_value) {
-            expected_event_count += 1;
-            console.log('Expected events counted:', expected_event_count)
-            if (expected_value) {
-                assertEqual(expected_value, expected_event_count)
-            }
-        }
-
-        function test_claimable_worker() {
-            console.log('Adding ready promise.');
-            navigator.serviceWorker.ready.then(function (
-                registration) {
-                assertNotEqual(null, registration);
-                console.log('(Expected) Registration ready promise',
-                    registration, ' with active worker ',
-                    registration.active);
-                assertNotEqual(null, registration.active);
-                registration.active.postMessage(
-                    'Registration ready received for claimable worker.');
-                count_event();
-                registration.unregister()
-                    .then(function (success) {
-                        // Even a claimed registration will successfully
-                        // unregister because unregistration takes effect after
-                        // the page is unloaded, so it's not blocked by being
-                        // the active service worker.
-                        console.log('(Expected) unregister success :',
-                            success);
-                        count_event(41);
-                    }, function (error) {
-                        console.log('(Unexpected) unregister ' +
-                            `${error}`, error);
-                        assertIncludes('SecurityError: ', `${error}`);
-                        notReached();
-                    });
-                console.log('unregister started.');
-
-            });
-
-            navigator.serviceWorker.register('service_worker_test_claimable.js', {
-                scope: './',
-            }).then(function (registration) {
-                worker = registration.installing;
-                if (worker) {
-                    console.log('claimable worker registered (installing).');
-                    worker.postMessage(
-                        'Registration successful, current worker state is ' +
-                        'installing.');
-                }
-                worker = registration.waiting;
-                if (worker) {
-                    console.log('claimable worker registered (waiting).');
-                    worker.postMessage(
-                        'Registration successful, current worker state is ' +
-                        'waiting.');
-                }
-                worker = registration.active;
-                if (worker) {
-                    console.log('claimable worker registered (active).');
-                    worker.postMessage(
-                        'Registration successful, current worker state is ' +
-                        'active.');
-                }
-             }, function (error) {
-                console.log('(Unexpected) :', error);
-                notReached();
-            });
-        }
-
-        console.log('Starting tests');
-        if ('serviceWorker' in navigator) {
-            navigator.serviceWorker.ready.then(function (registration) {
-                assertNotEqual(null, registration);
-                console.log('(Expected) Registration ready promise',
-                    registration, 'with active worker ', registration.active);
-                assertNotEqual(null, registration.active);
-                count_event();
-            });
-
-            navigator.serviceWorker.oncontrollerchange = function (event) {
-                console.log('Got oncontrollerchange event', event.target);
-                count_event(40);
-            }
-
-            navigator.serviceWorker.onmessage = function (event) {
-                console.log('Got onmessage event', event.target, event.data);
-            }
-
-            navigator.serviceWorker.register('http://..:..')
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test invalid script URL:', error);
-                    assertIncludes('TypeError', `${error}`);
-                    count_event(1);
-                });
-            navigator.serviceWorker.register('arg:service_worker_test.js')
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test script URL that is not http ' +
-                        'or https:', error);
-                    assertIncludes('TypeError', `${error}`);
-                    count_event(2);
-                });
-            navigator.serviceWorker.register('http:%2fservice_worker_test.js')
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test script URL with escaped slash:',
-                        error);
-                    assertIncludes('TypeError', `${error}`);
-                    count_event(3);
-                });
-            navigator.serviceWorker.register('service_worker_test.js', {
-                scope: 'http://..:..',
-            })
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test invalid scope URL:', error);
-                    assertIncludes('TypeError', `${error}`);
-                    count_event(4);
-                });
-            navigator.serviceWorker.register('service_worker_test.js', {
-                scope: 'arg:/',
-            })
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test scope URL that is not http ' +
-                        'or https:', error);
-                    assertIncludes('TypeError', `${error}`);
-                    count_event(5);
-                });
-            navigator.serviceWorker.register('service_worker_test.js', {
-                scope: '/%5c',
-            })
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test scope URL with escaped slash:',
-                        error);
-                    assertIncludes('TypeError', `${error}`);
-                    count_event(6);
-                });
-            // Repeat a few times to test the 'equivalent job' and finish job
-            // logic.
-            for (let repeated = 0; repeated < 15; repeated++) {
-                navigator.serviceWorker.register('http://www.example.com/', {
-                    scope: '/',
-                })
-                    .then(function (registration) {
-                        console.log('(Unexpected) :', registration);
-                        notReached();
-                    }, function (error) {
-                        console.log('(Expected) Test Script URL is not ' +
-                            `trusted: ${error}`, error);
-                        assertIncludes('SecurityError: ', `${error}`);
-                        count_event();
-                    });
-            }
-            navigator.serviceWorker.register('http://127.0.0.100:2345/')
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test script URL with different ' +
-                        `origin: ${error}`, error);
-                    assertIncludes('SecurityError: ', `${error}`);
-                    count_event();
-                });
-
-            navigator.serviceWorker.register('service_worker_test.js', {
-                scope: 'http://127.0.0.100:2345/',
-            })
-                .then(function (registration) {
-                    console.log('(Unexpected) :', registration);
-                    notReached();
-                }, function (error) {
-                    console.log('(Expected) Test scope URL with different ' +
-                        `origin: ${error}`, error);
-                    assertIncludes('SecurityError: ', `${error}`);
-                    count_event();
-                });
-
-            // Finally, test a succeeding registration.
-            navigator.serviceWorker.register('service_worker_test.js', {
-                scope: 'registration/scope',
-            })
-                .then(function (registration) {
-                    console.log('(Expected) Registration Succeeded:',
-                        registration);
-                    assertNotEqual(null, registration);
-                    // The default value for RegistrationOptions.type is
-                    // 'imports'.
-                    assertEqual('imports', registration.updateViaCache);
-                    assertTrue(registration.scope.endsWith(
-                        'registration/scope'));
-                    count_event(24);
-
-                    worker = registration.installing;
-                    if (worker) {
-                        worker.postMessage(
-                            'Registration with scope successful, current ' +
-                            'worker state is installing.');
-                    }
-                    worker = registration.waiting;
-                    if (worker) {
-                        worker.postMessage(
-                            'Registration with scope successful, current ' +
-                            'worker state is waiting.');
-                    }
-                    worker = registration.active;
-                    if (worker) {
-                        worker.postMessage(
-                            'Registration with scope successful, current ' +
-                            'worker state is active.');
-                    }
-
-                    registration.onupdatefound = function (event) {
-                        console.log('Got onupdatefound event',
-                            event.target.scope);
-                        assertTrue(event.target.scope.endsWith(
-                            'registration/scope'));
-                    }
-
-                    // Check that the registration has an activated worker after
-                    // some time. The delay used here should be long enough for
-                    // the service worker to complete activating and have the
-                    // state 'activated'. It has to be longer than the combined
-                    // delays in the install or activate event handlers.
-                    window.setTimeout(function () {
-                        // Since these events are asynchronous, the service
-                        // worker can be either of these states.
-                        console.log(
-                            'Registration active check after timeout',
-                            registration);
-                        assertNotEqual(null, registration.active);
-                        registration.active.postMessage(
-                            'Registration is active after waiting.');
-                        console.log('Registration active',
-                            registration.active, 'state:',
-                            registration.active.state);
-                        assertEqual('activated', registration.active.state);
-                        count_event(31);
-                        registration.active.onstatechange = function (event) {
-                            console.log('Got onstatechange event',
-                                event.target.state);
-                        }
-
-                        // Repeat a few times to test the 'equivalent job' and
-                        // finish job logic.
-                        for (let repeated = 0; repeated < 5; repeated++) {
-                            registration.update().then(function (registration) {
-                                console.log('(Expected) Registration update ' +
-                                    'Succeeded:', registration);
-                                assertNotEqual(null, registration);
-                                count_event();
-                            }, function (error) {
-                                console.log('(Unexpected) :', error);
-                                notReached();
-                            });
-                        }
-
-                        registration.unregister()
-                            .then(function (success) {
-                                console.log('(Expected) unregister success :',
-                                    success);
-                                count_event(37);
-                                // Finally, test getRegistration for the unregistered scope.
-                                navigator.serviceWorker.getRegistration(
-                                    'registration/scope')
-                                    .then(function (registration) {
-                                        console.log('(Expected) getRegistration Succeeded:',
-                                            registration);
-                                        assertTrue(null == registration ||
-                                            undefined == registration);
-                                        count_event(38);
-                                    }, function (error) {
-                                        console.log('(Unexpected) :', error);
-                                        notReached();
-                                    });
-
-                                test_claimable_worker();
-                            }, function (error) {
-                                console.log('(Unexpected) unregister ' +
-                                    `${error}`, error);
-                                assertIncludes('SecurityError: ', `${error}`);
-                                notReached();
-                            });
-                    }, 1000);
-
-                    // Test getRegistration for a non-registered scope.
-                    navigator.serviceWorker.getRegistration('/bo/gus')
-                        .then(function (registration) {
-                            console.log('(Expected) getRegistration Succeeded:',
-                                registration);
-                            assertTrue(null == registration ||
-                                undefined == registration);
-                            count_event();
-                        }, function (error) {
-                            console.log('(Unexpected) :', error);
-                            notReached();
-                        });
-
-                    // Test getRegistration for a deeper registered scope.
-                    navigator.serviceWorker.getRegistration(
-                        'registration/scope/deeper')
-                        .then(function (registration) {
-                            console.log('(Expected) getRegistration Succeeded:',
-                                registration);
-                            assertNotEqual(null, registration);
-                            assertEqual('imports', registration.updateViaCache);
-                            assertTrue(registration.scope.endsWith(
-                                'registration/scope'));
-                            count_event();
-                        }, function (error) {
-                            console.log('(Unexpected) :', error);
-                            notReached();
-                        });
-
-                    // Test getRegistration for a shallower registered scope.
-                    navigator.serviceWorker.getRegistration('registration')
-                        .then(function (registration) {
-                            console.log('(Expected) getRegistration Succeeded:',
-                                registration);
-                            assertTrue(null == registration ||
-                                undefined == registration);
-                            count_event();
-                        }, function (error) {
-                            console.log('(Unexpected) :', error);
-                            notReached();
-                        });
-
-                    // Test getRegistration for a non-registered scope.
-                    navigator.serviceWorker.getRegistration()
-                        .then(function (registration) {
-                            console.log('(Expected) getRegistration Succeeded:',
-                                registration);
-                            // TODO(b/234659851): Investigate whether this
-                            // should return a registration or not, in this case
-                            // where there is a registration with a scope.
-                            assertTrue(null == registration ||
-                                undefined == registration);
-                            count_event();
-                        }, function (error) {
-                            console.log('(Unexpected) :', error);
-                            notReached();
-                        });
-
-                    // Finally, test getRegistration for a registered scope.
-                    navigator.serviceWorker.getRegistration(
-                        'registration/scope')
-                        .then(function (registration) {
-                            console.log('(Expected) getRegistration Succeeded:',
-                                registration);
-                            assertNotEqual(null, registration);
-                            assertEqual('imports', registration.updateViaCache);
-                            assertTrue(registration.scope.endsWith(
-                                'registration/scope'));
-                            count_event();
-                        }, function (error) {
-                            console.log('(Unexpected) :', error);
-                            notReached();
-                        });
-                }, function (error) {
-                    console.log('(Unexpected) :', error);
-                    notReached();
-                });
-        }
-
-        assertEqual(undefined, self.clients);
-
-        console.log('Done starting tests');
-        setupFinished();
-        // This delay has to be long enough to guarantee that the test has
-        // finished.
-        window.setTimeout(
-            () => {
-                console.log('Events:', expected_event_count)
-                assertEqual(41, expected_event_count);
-                onEndTest();
-            }, 7000);
-
-    </script>
+    <script src='service_worker_test_script.js'></script>
 </body>
 
 </html>
diff --git a/cobalt/black_box_tests/testdata/service_worker_test_script.js b/cobalt/black_box_tests/testdata/service_worker_test_script.js
new file mode 100644
index 0000000..0428eab
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/service_worker_test_script.js
@@ -0,0 +1,506 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+var expected_event_count = 0;
+var expected_event_count_offset = 0;
+
+function total_events() {
+    return expected_event_count + expected_event_count_offset;
+}
+
+function count_event(expected_value) {
+    expected_event_count += 1;
+    if (expected_value) {
+        assertEqual(expected_value, expected_event_count)
+    }
+    return expected_event_count;
+}
+
+function next_function(next_func) {
+    expected_event_count_offset += expected_event_count;
+    expected_event_count = 0;
+    console.log(next_func.name);
+    setTimeout(function () { next_func(); }, 0);
+}
+
+assertEqual(undefined, self.clients);
+
+if ('serviceWorker' in navigator) {
+    console.log('Starting tests');
+    test_main();
+}
+
+setupFinished();
+// This 7 second delay has to be long enough to guarantee that the test has
+// finished and allow for time to see any spurious events that may arrive later.
+window.setTimeout(
+    () => {
+        console.log('Total Events:', total_events())
+        assertEqual(44, total_events());
+        onEndTest();
+        // Exit 3 seconds after ending the test instead of relying on the test
+        // runner to force the exit.
+        setTimeout(function () { window.close(); }, 3000);
+    }, 7000);
+
+function test_main() {
+    navigator.serviceWorker.ready.then(function (registration) {
+        assertNotEqual(null, registration);
+        console.log('(Expected) Registration ready promise',
+            registration, 'with active worker ', registration.active);
+        assertNotEqual(null, registration.active);
+        count_event();
+    });
+
+    navigator.serviceWorker.oncontrollerchange = function (event) {
+        console.log('Got oncontrollerchange event', event.target);
+        count_event(17);
+    }
+
+    navigator.serviceWorker.onmessage = function (event) {
+        console.log('Got onmessage event', event.target, event.data);
+    }
+
+    next_function(test_invalid_script_url);
+}
+
+function test_invalid_script_url() {
+    navigator.serviceWorker.register('http://..:..'
+    ).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log(`(Expected) Test invalid script URL: ${error}`, error);
+        assertIncludes('TypeError', `${error}`);
+        count_event(1);
+        next_function(test_script_url_that_is_not_http);
+    });
+}
+
+function test_script_url_that_is_not_http() {
+    navigator.serviceWorker.register('arg:service_worker_test_worker.js'
+    ).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log('(Expected) Test script URL that is not http ' +
+            `or https: ${error}`, error);
+        assertIncludes('TypeError', `${error}`);
+        count_event(1);
+        next_function(test_script_url_with_escaped_slash);
+    });
+}
+
+function test_script_url_with_escaped_slash() {
+    navigator.serviceWorker.register('http:%2fservice_worker_test_worker.js'
+    ).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log('(Expected) Test script URL with escaped slash:',
+            `${error}`, error);
+        assertIncludes('TypeError', `${error}`);
+        count_event(1);
+        next_function(test_invalid_scope_url);
+    });
+}
+
+function test_invalid_scope_url() {
+    navigator.serviceWorker.register('service_worker_test_worker.js', {
+        scope: 'http://..:..',
+    }).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log(`(Expected) Test invalid scope URL: ${error}`, error);
+        assertIncludes('TypeError', `${error}`);
+        count_event(1);
+        next_function(test_scope_url_that_is_not_http);
+    });
+}
+
+function test_scope_url_that_is_not_http() {
+    navigator.serviceWorker.register('service_worker_test_worker.js', {
+        scope: 'arg:/',
+    }).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log('(Expected) Test scope URL that is not http ' +
+            `or https: ${error}`, error);
+        assertIncludes('TypeError', `${error}`);
+        count_event(1);
+        next_function(test_scope_url_that_is_not_allowed);
+    });
+}
+
+function test_scope_url_that_is_not_allowed() {
+    navigator.serviceWorker.register('service_worker_test_worker.js', {
+        scope: '/foo',
+    }).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log('(Expected) Test scope URL that is not allowed:' +
+            `${error}`, error);
+        assertIncludes('SecurityError', `${error}`);
+        count_event(1);
+        next_function(test_scope_url_with_escaped_slash());
+    });
+}
+
+function test_scope_url_with_escaped_slash() {
+    navigator.serviceWorker.register('service_worker_test_worker.js', {
+        scope: '/%5c',
+    }).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log('(Expected) Test scope URL with escaped slash:',
+            `${error}`, error);
+        assertIncludes('TypeError', `${error}`);
+        count_event(1);
+        next_function(test_script_url_not_trusted_and_equivalent_job);
+    });
+}
+
+function test_script_url_not_trusted_and_equivalent_job() {
+    // Repeat a few times to test the 'equivalent job' and finish job
+    // logic.
+    var repeat_count = 15;
+    for (let repeated = 0; repeated < repeat_count; repeated++) {
+        navigator.serviceWorker.register('http://www.example.com/', {
+            scope: '/',
+        }).then(function (registration) {
+            console.log('(Unexpected):', registration);
+            notReached();
+        }, function (error) {
+            console.log(`(Expected): ${error}`, error);
+            assertIncludes('SecurityError: ', `${error}`);
+            if (count_event() >= repeat_count) {
+                next_function(test_script_url_and_referrer_origin_not_same);
+            }
+        });
+    }
+}
+
+function test_script_url_and_referrer_origin_not_same() {
+    navigator.serviceWorker.register('http://127.0.0.100:2345/')
+        .then(function (registration) {
+            console.log('(Unexpected):', registration);
+            notReached();
+        }, function (error) {
+            console.log(`(Expected): ${error}`, error);
+            assertIncludes('SecurityError: ', `${error}`);
+            count_event(1);
+            next_function(test_scope_url_and_referrer_origin_not_same);
+        });
+}
+
+function test_scope_url_and_referrer_origin_not_same() {
+    navigator.serviceWorker.register('service_worker_test_worker.js', {
+        scope: 'http://127.0.0.100:2345/',
+    }).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log(`(Expected): ${error}`, error);
+        assertIncludes('SecurityError: ', `${error}`);
+        count_event(1);
+        next_function(test_url_not_javascript_mime_type());
+    });
+}
+
+function test_url_not_javascript_mime_type() {
+    navigator.serviceWorker.register('service_worker_test.html'
+    ).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log('(Expected) Test script URL that is not JavaScript MIME ' +
+            `type: ${error}`, error);
+        assertIncludes('SecurityError', `${error}`);
+        count_event(1);
+        next_function(test_url_not_exist);
+    });
+}
+
+function test_url_not_exist() {
+    navigator.serviceWorker.register('service_worker_test_nonexist.js'
+    ).then(function (registration) {
+        console.log('(Unexpected):', registration);
+        notReached();
+    }, function (error) {
+        console.log('(Expected) Test script URL that does not exist:' +
+            `${error}`, error);
+        assertIncludes('NetworkError', `${error}`);
+        count_event(1);
+
+        // Finally, test a succeeding registration.
+        next_function(test_successful_registration());
+    });
+}
+
+function test_successful_registration() {
+    console.log('test_successful_registration()');
+    navigator.serviceWorker.register('service_worker_test_worker.js', {
+        scope: '/bar/registration/scope',
+    }).then(function (registration) {
+        console.log('(Expected) Registration Succeeded:',
+            registration);
+        assertNotEqual(null, registration);
+        // The default value for RegistrationOptions.type is
+        // 'imports'.
+        assertEqual('imports', registration.updateViaCache);
+        assertTrue(registration.scope.endsWith(
+            'bar/registration/scope'));
+        count_event(1);
+
+        worker = registration.installing;
+        if (worker) {
+            worker.postMessage(
+                'Registration with scope successful, ' +
+                'current worker state is installing.');
+        }
+        worker = registration.waiting;
+        if (worker) {
+            worker.postMessage(
+                'Registration with scope successful, ' +
+                'current worker state is waiting.');
+        }
+        worker = registration.active;
+        if (worker) {
+            worker.postMessage(
+                'Registration with scope successful, ' +
+                'current worker state is active.');
+        }
+
+        registration.onupdatefound = function (event) {
+            console.log('Got onupdatefound event',
+                event.target.scope);
+            assertTrue(event.target.scope.endsWith(
+                'bar/registration/scope'));
+        }
+
+        // Check that the registration has an activated worker after
+        // some time. The delay used here should be long enough for
+        // the service worker to complete activating and have the
+        // state 'activated'. It has to be longer than the combined
+        // delays in the install or activate event handlers.
+        window.setTimeout(function () {
+            // Since these events are asynchronous, the service
+            // worker can be either of these states.
+            console.log(
+                'Registration active check after timeout',
+                registration);
+            assertNotEqual(null, registration.active);
+            registration.active.postMessage(
+                'Registration is active after waiting.');
+            console.log('Registration active',
+                registration.active, 'state:',
+                registration.active.state);
+            assertEqual('activated',
+                registration.active.state);
+            count_event(8);
+            registration.active.onstatechange =
+                function (event) {
+                    console.log('Got onstatechange event',
+                        event.target.state);
+                }
+
+            // Repeat a few times to test the 'equivalent job' and
+            // finish job logic.
+            for (let repeated = 0; repeated < 5; repeated++) {
+                registration.update().then(function (registration) {
+                    console.log('(Expected) Registration update ' +
+                        'Succeeded:', registration);
+                    assertNotEqual(null, registration);
+                    count_event();
+                }, function (error) {
+                    console.log(`(Unexpected): ${error}`, error);
+                    notReached();
+                });
+            }
+
+            registration.unregister()
+                .then(function (success) {
+                    console.log('(Expected) unregister success :',
+                        success);
+                    count_event(14);
+                    // Finally, test getRegistration for the
+                    // unregistered scope.
+                    navigator.serviceWorker.getRegistration(
+                        'bar/registration/scope')
+                        .then(function (registration) {
+                            console.log('(Expected) ' +
+                                'getRegistration Succeeded: ',
+                                registration);
+                            assertTrue(null == registration ||
+                                undefined == registration);
+                            count_event(15);
+                        }, function (error) {
+                            console.log(`(Unexpected): ${error}`,
+                                error);
+                            notReached();
+                        });
+
+                    test_claimable_worker();
+                }, function (error) {
+                    console.log('(Unexpected) unregister ' +
+                        `${error}`, error);
+                    assertIncludes('SecurityError: ', `${error}`);
+                    notReached();
+                });
+        }, 1000);
+
+        // Test getRegistration for a non-registered scope.
+        navigator.serviceWorker.getRegistration('/bo/gus')
+            .then(function (registration) {
+                console.log('(Expected) getRegistration Succeeded:',
+                    registration);
+                assertTrue(null == registration ||
+                    undefined == registration);
+                count_event();
+            }, function (error) {
+                console.log(`(Unexpected): ${error}`, error);
+                notReached();
+            });
+
+        // Test getRegistration for a deeper registered scope.
+        navigator.serviceWorker.getRegistration(
+            '/bar/registration/scope/deeper')
+            .then(function (registration) {
+                console.log('(Expected) getRegistration Succeeded:',
+                    registration);
+                assertNotEqual(null, registration);
+                assertEqual('imports', registration.updateViaCache);
+                assertTrue(registration.scope.endsWith(
+                    'bar/registration/scope'));
+                count_event();
+            }, function (error) {
+                console.log(`(Unexpected): ${error}`, error);
+                notReached();
+            });
+
+        // Test getRegistration for a shallower registered scope.
+        navigator.serviceWorker.getRegistration('registration')
+            .then(function (registration) {
+                console.log('(Expected) getRegistration Succeeded:',
+                    registration);
+                assertTrue(null == registration ||
+                    undefined == registration);
+                count_event();
+            }, function (error) {
+                console.log(`(Unexpected): ${error}`, error);
+                notReached();
+            });
+
+        // Test getRegistration for a non-registered scope.
+        navigator.serviceWorker.getRegistration()
+            .then(function (registration) {
+                console.log('(Expected) getRegistration Succeeded:',
+                    registration);
+                // TODO(b/234659851): Investigate whether this
+                // should return a registration or not, in this case
+                // where there is a registration with a scope.
+                assertTrue(null == registration ||
+                    undefined == registration);
+                count_event();
+            }, function (error) {
+                console.log(`(Unexpected): ${error}`, error);
+                notReached();
+            });
+
+        // Finally, test getRegistration for a registered scope.
+        navigator.serviceWorker.getRegistration(
+            '/bar/registration/scope')
+            .then(function (registration) {
+                console.log('(Expected) getRegistration Succeeded:',
+                    registration);
+                assertNotEqual(null, registration);
+                assertEqual('imports', registration.updateViaCache);
+                assertTrue(registration.scope.endsWith(
+                    'bar/registration/scope'));
+                count_event();
+            }, function (error) {
+                console.log(`(Unexpected): ${error}`, error);
+                notReached();
+            });
+    }, function (error) {
+        console.log(`(Unexpected): ${error}`, error);
+        notReached();
+    });
+}
+
+function test_claimable_worker() {
+    console.log('test_claimable_worker()');
+    console.log('Adding ready promise.');
+    navigator.serviceWorker.ready.then(function (
+        registration) {
+        assertNotEqual(null, registration);
+        console.log('(Expected) Registration ready promise',
+            registration, ' with active worker ',
+            registration.active);
+        assertNotEqual(null, registration.active);
+        registration.active.postMessage(
+            'Registration ready received for claimable worker.');
+        count_event();
+        registration.unregister()
+            .then(function (success) {
+                // Even a claimed registration will successfully
+                // unregister because unregistration takes effect after
+                // the page is unloaded, so it's not blocked by being
+                // the active service worker.
+                console.log('(Expected) unregister success :',
+                    success);
+                count_event(18);
+            }, function (error) {
+                console.log('(Unexpected) unregister ' +
+                    `${error}`, error);
+                assertIncludes('SecurityError: ', `${error}`);
+                notReached();
+            });
+        console.log('unregister started.');
+
+    });
+
+    navigator.serviceWorker.register('service_worker_test_claimable.js', {
+        scope: './',
+    }).then(function (registration) {
+        worker = registration.installing;
+        if (worker) {
+            console.log('claimable worker registered (installing).');
+            worker.postMessage(
+                'Registration successful, current worker state is ' +
+                'installing.');
+        }
+        worker = registration.waiting;
+        if (worker) {
+            console.log('claimable worker registered (waiting).');
+            worker.postMessage(
+                'Registration successful, current worker state is ' +
+                'waiting.');
+        }
+        worker = registration.active;
+        if (worker) {
+            console.log('claimable worker registered (active).');
+            worker.postMessage(
+                'Registration successful, current worker state is ' +
+                'active.');
+        }
+    }, function (error) {
+        console.log(`(Unexpected): ${error}`, error);
+        notReached();
+    });
+}
diff --git a/cobalt/black_box_tests/testdata/service_worker_test.js b/cobalt/black_box_tests/testdata/service_worker_test_worker.js
similarity index 100%
rename from cobalt/black_box_tests/testdata/service_worker_test.js
rename to cobalt/black_box_tests/testdata/service_worker_test_worker.js
diff --git a/cobalt/black_box_tests/testdata/worker_csp_test.html b/cobalt/black_box_tests/testdata/worker_csp_test.html
new file mode 100644
index 0000000..fdb1cea
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/worker_csp_test.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<!--
+  Copyright 2022 The Cobalt Authors. All Rights Reserved.
+
+  Licensed under the Apache License, Version 2.0 (the "License");
+  you may not use this file except in compliance with the License.
+  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+
+<head>
+  <title>Verify that XHR on workers correctly follows CSP</title>
+  <script src='black_box_js_test_utils.js'></script>
+</head>
+
+<body style="background-color: white;">
+  <h1>
+    <span>ID element</span>
+  </h1>
+  <script>
+  var worker = new Worker('worker_csp_test.js');
+  worker.onmessage = function (event) {
+    notReached();
+  };
+
+  window.setTimeout(
+    () => {
+      worker.terminate();
+      onEndTest();
+    }, 250);
+  </script>
+</body>
diff --git a/cobalt/black_box_tests/testdata/worker_csp_test.js b/cobalt/black_box_tests/testdata/worker_csp_test.js
new file mode 100644
index 0000000..58874aa
--- /dev/null
+++ b/cobalt/black_box_tests/testdata/worker_csp_test.js
@@ -0,0 +1,6 @@
+const xhr = new XMLHttpRequest();
+// Script should fail and stop execution at this step due to SecurityViolation.
+xhr.open("GET", "http://www.google.com");
+xhr.send();
+
+self.postMessage("Should not be reached!");
diff --git a/cobalt/black_box_tests/tests/allow_eval.py b/cobalt/black_box_tests/tests/allow_eval.py
index 503ffbb..4f5137d 100644
--- a/cobalt/black_box_tests/tests/allow_eval.py
+++ b/cobalt/black_box_tests/tests/allow_eval.py
@@ -24,7 +24,7 @@
 class AllowEvalTest(black_box_tests.BlackBoxTestCase):
   """Ensure that client page can use eval() when CSP is missing."""
 
-  def test_simple(self):
+  def test_allow_eval(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/allow_eval.html')
diff --git a/cobalt/black_box_tests/tests/cancel_sync_loads_when_suspended.py b/cobalt/black_box_tests/tests/cancel_sync_loads_when_suspended.py
index 4036853..a40956a 100644
--- a/cobalt/black_box_tests/tests/cancel_sync_loads_when_suspended.py
+++ b/cobalt/black_box_tests/tests/cancel_sync_loads_when_suspended.py
@@ -89,7 +89,7 @@
     except:  # pylint: disable=bare-except
       traceback.print_exc()
 
-  def test_simple(self):
+  def test_cancel_sync_loads_when_suspended(self):
 
     # Step 2. Start Cobalt, and point it to the socket created in Step 1.
     try:
diff --git a/cobalt/black_box_tests/tests/conceal_visibility.py b/cobalt/black_box_tests/tests/conceal_visibility.py
index 5df63de..b4d76f1 100644
--- a/cobalt/black_box_tests/tests/conceal_visibility.py
+++ b/cobalt/black_box_tests/tests/conceal_visibility.py
@@ -24,7 +24,7 @@
 class ConcealVisibilityTest(black_box_tests.BlackBoxTestCase):
   """Verify correct visibility changes during and after conceal event."""
 
-  def test_simple(self):
+  def test_conceal_visibility(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/conceal_visibility.html')
diff --git a/cobalt/black_box_tests/tests/disable_eval_with_csp.py b/cobalt/black_box_tests/tests/disable_eval_with_csp.py
index 3fd19bb..82d1076 100644
--- a/cobalt/black_box_tests/tests/disable_eval_with_csp.py
+++ b/cobalt/black_box_tests/tests/disable_eval_with_csp.py
@@ -24,7 +24,7 @@
 class DisableEvalWithCSPTest(black_box_tests.BlackBoxTestCase):
   """Ensure that Cobalt prohibits eval() when CSP is present in client page."""
 
-  def test_simple(self):
+  def test_disable_eval_with_csp(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/disable_eval_with_csp.html')
diff --git a/cobalt/black_box_tests/tests/freeze_timers.py b/cobalt/black_box_tests/tests/freeze_timers.py
index cbcbce3..ba56d58 100644
--- a/cobalt/black_box_tests/tests/freeze_timers.py
+++ b/cobalt/black_box_tests/tests/freeze_timers.py
@@ -26,7 +26,7 @@
 class FreezeVisibilityTest(black_box_tests.BlackBoxTestCase):
   """Verify correct timer suspend and resume during and after freeze event."""
 
-  def test_simple(self):
+  def test_freeze_visibility(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/freeze_timers.html')
diff --git a/cobalt/black_box_tests/tests/freeze_visibility.py b/cobalt/black_box_tests/tests/freeze_visibility.py
index d10c95c..e3b9f51 100644
--- a/cobalt/black_box_tests/tests/freeze_visibility.py
+++ b/cobalt/black_box_tests/tests/freeze_visibility.py
@@ -24,7 +24,7 @@
 class FreezeVisibilityTest(black_box_tests.BlackBoxTestCase):
   """Verify correct visibility changes during and after freeze event."""
 
-  def test_simple(self):
+  def test_freeze_visibility(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/freeze_visibility.html')
diff --git a/cobalt/black_box_tests/tests/http_cache.py b/cobalt/black_box_tests/tests/http_cache.py
index 3b24fc5..43958ec 100644
--- a/cobalt/black_box_tests/tests/http_cache.py
+++ b/cobalt/black_box_tests/tests/http_cache.py
@@ -35,7 +35,7 @@
     parsed_path = urlparse(self.path)
 
     if parsed_path.path.startswith('/testdata/http_cache_test_resources/'):
-      time.sleep(10)
+      time.sleep(3)
 
     return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
 
@@ -43,7 +43,7 @@
 class HttpCacheTest(black_box_tests.BlackBoxTestCase):
   """Load resources, then reload the page and verify."""
 
-  def test_simple(self):
+  def test_http_cache(self):
 
     with ThreadedWebServer(
         binding_address=self.GetBindingAddress(),
diff --git a/cobalt/black_box_tests/tests/override_ua_parameters.py b/cobalt/black_box_tests/tests/override_ua_parameters.py
index 01768ae..3dec188 100644
--- a/cobalt/black_box_tests/tests/override_ua_parameters.py
+++ b/cobalt/black_box_tests/tests/override_ua_parameters.py
@@ -53,7 +53,7 @@
 class OverrideUAParametersTest(black_box_tests.BlackBoxTestCase):
   """Test overriding UA parameters."""
 
-  def test_simple(self):
+  def test_override_ua_parameters(self):
     """Set UA parameters when launching Cobalt."""
 
     with ThreadedWebServer(
diff --git a/cobalt/black_box_tests/tests/persistent_cookie.py b/cobalt/black_box_tests/tests/persistent_cookie.py
index 209bc09..ab30bb4 100644
--- a/cobalt/black_box_tests/tests/persistent_cookie.py
+++ b/cobalt/black_box_tests/tests/persistent_cookie.py
@@ -29,7 +29,7 @@
 
   # The same page has to be used since cookie are stored per URL.
 
-  def test_simple(self):
+  def test_persistent_cookie(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/persistent_cookie.html')
diff --git a/cobalt/black_box_tests/tests/preload_font.py b/cobalt/black_box_tests/tests/preload_font.py
index 5f8a5ae..b43a00a 100644
--- a/cobalt/black_box_tests/tests/preload_font.py
+++ b/cobalt/black_box_tests/tests/preload_font.py
@@ -28,7 +28,7 @@
 class PreloadFontTest(black_box_tests.BlackBoxTestCase):
   """Verify that fonts are loaded correctly after preload event."""
 
-  def test_simple(self):
+  def test_preload_font(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/preload_font.html')
diff --git a/cobalt/black_box_tests/tests/preload_launch_parameter.py b/cobalt/black_box_tests/tests/preload_launch_parameter.py
index a0675af..e23fb38 100644
--- a/cobalt/black_box_tests/tests/preload_launch_parameter.py
+++ b/cobalt/black_box_tests/tests/preload_launch_parameter.py
@@ -24,7 +24,7 @@
 class PreloadLaunchParameterTest(black_box_tests.BlackBoxTestCase):
   """Set a JS timer that expires after exiting preload mode."""
 
-  def test_simple(self):
+  def test_preload_launch_parameter(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/preload_launch_parameter.html')
diff --git a/cobalt/black_box_tests/tests/preload_visibility.py b/cobalt/black_box_tests/tests/preload_visibility.py
index 7d75050..6eab66f 100644
--- a/cobalt/black_box_tests/tests/preload_visibility.py
+++ b/cobalt/black_box_tests/tests/preload_visibility.py
@@ -24,7 +24,7 @@
 class PreloadVisibilityTest(black_box_tests.BlackBoxTestCase):
   """Set a JS timer that expires after exiting preload mode."""
 
-  def test_simple(self):
+  def test_preload_visibility(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/preload_visibility.html')
diff --git a/cobalt/black_box_tests/tests/retry_async_script_loads_after_suspend.py b/cobalt/black_box_tests/tests/retry_async_script_loads_after_suspend.py
index 3359b4f..1420463 100644
--- a/cobalt/black_box_tests/tests/retry_async_script_loads_after_suspend.py
+++ b/cobalt/black_box_tests/tests/retry_async_script_loads_after_suspend.py
@@ -100,7 +100,7 @@
     except:  # pylint: disable=bare-except
       traceback.print_exc()
 
-  def test_simple(self):
+  def test_retry_async_script_loads_after_suspend(self):
 
     # Step 2. Start Cobalt, and point it to the socket created in Step 1.
     try:
diff --git a/cobalt/black_box_tests/tests/service_worker_message_test.py b/cobalt/black_box_tests/tests/service_worker_message_test.py
new file mode 100644
index 0000000..b4c4675
--- /dev/null
+++ b/cobalt/black_box_tests/tests/service_worker_message_test.py
@@ -0,0 +1,30 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests Service Worker Messaging functionality."""
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
+
+
+class ServiceWorkerMessageTest(black_box_tests.BlackBoxTestCase):
+  """Test basic Service Worker functionality."""
+
+  def test_service_worker_message(self):
+
+    with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+      url = server.GetURL(file_name='testdata/service_worker_message_test.html')
+
+      with self.CreateCobaltRunner(url=url) as runner:
+        runner.WaitForJSTestsSetup()
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/tests/service_worker_test.py b/cobalt/black_box_tests/tests/service_worker_test.py
index 72fa974..f66b2dc 100644
--- a/cobalt/black_box_tests/tests/service_worker_test.py
+++ b/cobalt/black_box_tests/tests/service_worker_test.py
@@ -13,16 +13,69 @@
 # limitations under the License.
 """Tests basic Service Worker functionality."""
 
+import os
+from six.moves import SimpleHTTPServer
+
 from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import MakeRequestHandlerClass
 from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer
 
+# The base path of the requested assets is the parent directory.
+_SERVER_ROOT_PATH = os.path.join(os.path.dirname(__file__), os.pardir)
+
+
+class ServiceWorkerRequestDetector(MakeRequestHandlerClass(_SERVER_ROOT_PATH)):
+  """Proxies everything to SimpleHTTPRequestHandler, except some paths."""
+
+  def end_headers(self):
+    self.send_my_headers()
+
+    SimpleHTTPServer.SimpleHTTPRequestHandler.end_headers(self)
+
+  def send_header(self, header, value):
+    # Ensure that the Content-Type for paths ending in '.js' are always
+    # 'text/javascript'.
+    if header == 'Content-Type' and self.path.endswith('.js'):
+      SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(
+          self, header, 'text/javascript')
+    else:
+      SimpleHTTPServer.SimpleHTTPRequestHandler.send_header(self, header, value)
+
+  def send_my_headers(self):
+    # Add 'Service-Worker-Allowed' header for the main service worker scripts.
+    if self.path.endswith('/service_worker_test_worker.js'):
+      self.send_header('Service-Worker-Allowed', '/bar')
+
+  def do_GET(self):  # pylint: disable=invalid-name
+    """Handles HTTP GET requests for resources."""
+    # Check if the request for the main service worker script has the
+    # 'Service-Worker' header.
+    if self.path.endswith(
+        '/service_worker_test_worker.js') or self.path.endswith(
+            '/service_worker_test_claimable.js'):
+      service_worker_request_header = self.headers.get('Service-Worker', '')
+      expected_service_worker_request_header = 'script'
+
+      if not (service_worker_request_header
+              == expected_service_worker_request_header):
+        raise ValueError('Service-Worker HTTP request header value does not '
+                         'match with expected value.\n'
+                         'Header value:%s\n'
+                         'Expected value:%s' %
+                         (service_worker_request_header,
+                          expected_service_worker_request_header))
+
+    return SimpleHTTPServer.SimpleHTTPRequestHandler.do_GET(self)
+
 
 class ServiceWorkerTest(black_box_tests.BlackBoxTestCase):
   """Test basic Service Worker functionality."""
 
-  def test_simple(self):
+  def test_service_worker(self):
 
-    with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
+    with ThreadedWebServer(
+        ServiceWorkerRequestDetector,
+        binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/service_worker_test.html')
 
       with self.CreateCobaltRunner(url=url) as runner:
diff --git a/cobalt/black_box_tests/tests/signal_handler_doesnt_crash.py b/cobalt/black_box_tests/tests/signal_handler_doesnt_crash.py
index 8374c3f..ce2cc14 100644
--- a/cobalt/black_box_tests/tests/signal_handler_doesnt_crash.py
+++ b/cobalt/black_box_tests/tests/signal_handler_doesnt_crash.py
@@ -30,7 +30,7 @@
     if 'linux' not in self.launcher_params.platform:  # pylint: disable=unsupported-membership-test
       self.skipTest('This test needs POSIX system signal handlers')
 
-  def test_simple(self):
+  def test_signal_handler_doesnt_crash(self):
     with self.CreateCobaltRunner(url='', target_params=[]) as runner:
       runner.WaitForUrlLoadedEvents()
 
diff --git a/cobalt/black_box_tests/tests/suspend_visibility.py b/cobalt/black_box_tests/tests/suspend_visibility.py
index 7055d3f..33205a1 100644
--- a/cobalt/black_box_tests/tests/suspend_visibility.py
+++ b/cobalt/black_box_tests/tests/suspend_visibility.py
@@ -24,7 +24,7 @@
 class SuspendVisibilityTest(black_box_tests.BlackBoxTestCase):
   """Verify correct visibility changes during and after suspend event."""
 
-  def test_simple(self):
+  def test_suspend_visibility(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/suspend_visibility.html')
diff --git a/cobalt/black_box_tests/tests/text_encoding_test.py b/cobalt/black_box_tests/tests/text_encoding_test.py
index 56d58ae..e1c3563 100644
--- a/cobalt/black_box_tests/tests/text_encoding_test.py
+++ b/cobalt/black_box_tests/tests/text_encoding_test.py
@@ -20,7 +20,7 @@
 class TextEncodingTest(black_box_tests.BlackBoxTestCase):
   """Test basic TextEncoder and TextDecoder functionality."""
 
-  def test_simple(self):
+  def test_text_encoding(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/text_encoding_test.html')
diff --git a/cobalt/black_box_tests/tests/timer_hit_after_preload.py b/cobalt/black_box_tests/tests/timer_hit_after_preload.py
index f48a93f..5a97dfc 100644
--- a/cobalt/black_box_tests/tests/timer_hit_after_preload.py
+++ b/cobalt/black_box_tests/tests/timer_hit_after_preload.py
@@ -24,7 +24,7 @@
 class TimerAfterPreloadTest(black_box_tests.BlackBoxTestCase):
   """Set a JS timer that expires after exiting preload mode."""
 
-  def test_simple(self):
+  def test_timer_after_preload(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/timer_hit_after_preload.html')
diff --git a/cobalt/black_box_tests/tests/timer_hit_in_preload.py b/cobalt/black_box_tests/tests/timer_hit_in_preload.py
index edb0c8f..fc88ec7 100644
--- a/cobalt/black_box_tests/tests/timer_hit_in_preload.py
+++ b/cobalt/black_box_tests/tests/timer_hit_in_preload.py
@@ -24,7 +24,7 @@
 class TimerInPreloadTest(black_box_tests.BlackBoxTestCase):
   """Set a JS timer that expires during preload mode."""
 
-  def test_simple(self):
+  def test_timer_in_preload(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/timer_hit_in_preload.html')
diff --git a/cobalt/black_box_tests/tests/web_platform_tests.py b/cobalt/black_box_tests/tests/web_platform_tests.py
index 2c4fe56..4b64734 100644
--- a/cobalt/black_box_tests/tests/web_platform_tests.py
+++ b/cobalt/black_box_tests/tests/web_platform_tests.py
@@ -31,7 +31,7 @@
     if self.launcher_params.config not in ['debug', 'devel']:
       self.skipTest('Can only run web platform tests on debug or devel config.')
 
-  def test_simple(self):
+  def test_web_platform(self):
     with WebPlatformTestServer(
         binding_address=self.GetBindingAddress(),
         wpt_http_port=self.GetWptHttpPort()):
diff --git a/cobalt/black_box_tests/tests/web_worker_test.py b/cobalt/black_box_tests/tests/web_worker_test.py
index 1ac8046..0dcb550 100644
--- a/cobalt/black_box_tests/tests/web_worker_test.py
+++ b/cobalt/black_box_tests/tests/web_worker_test.py
@@ -20,7 +20,7 @@
 class WebWorkerTest(black_box_tests.BlackBoxTestCase):
   """Test basic Web Worker functionality."""
 
-  def test_simple(self):
+  def test_web_worker(self):
 
     with ThreadedWebServer(binding_address=self.GetBindingAddress()) as server:
       url = server.GetURL(file_name='testdata/web_worker_test.html')
diff --git a/cobalt/black_box_tests/tests/worker_csp_test.py b/cobalt/black_box_tests/tests/worker_csp_test.py
new file mode 100644
index 0000000..18b6511
--- /dev/null
+++ b/cobalt/black_box_tests/tests/worker_csp_test.py
@@ -0,0 +1,44 @@
+# Copyright 2022 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Tests workers handling CSP Violations."""
+
+import os
+
+from cobalt.black_box_tests import black_box_tests
+from cobalt.black_box_tests.threaded_web_server import ThreadedWebServer, MakeCustomHeaderRequestHandlerClass
+
+paths_to_headers = {
+    'worker_csp_test.html': {
+        'Content-Security-Policy':
+            "script-src 'unsafe-inline' 'self'; connect-src 'self';"
+    },
+    'worker_csp_test.js': {
+        'Content-Security-Policy': "connect-src 'self';"
+    }
+}
+
+
+class WorkerCspTest(black_box_tests.BlackBoxTestCase):
+  """Verify correct correct CSP behavior."""
+
+  def test_worker_csp(self):
+    path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
+    with ThreadedWebServer(
+        binding_address=self.GetBindingAddress(),
+        handler=MakeCustomHeaderRequestHandlerClass(
+            path, paths_to_headers)) as server:
+      url = server.GetURL(file_name='testdata/worker_csp_test.html')
+
+      with self.CreateCobaltRunner(url=url) as runner:
+        self.assertTrue(runner.JSTestsSucceeded())
diff --git a/cobalt/black_box_tests/threaded_web_server.py b/cobalt/black_box_tests/threaded_web_server.py
index 1064355..1e173e3 100644
--- a/cobalt/black_box_tests/threaded_web_server.py
+++ b/cobalt/black_box_tests/threaded_web_server.py
@@ -60,6 +60,87 @@
   return TestDataHTTPRequestHandler
 
 
+def MakeCustomHeaderRequestHandlerClass(base_path, paths_to_headers):
+  """RequestHandler that serves files that reside relative to base_path.
+
+  Args:
+    base_path: A path considered to be the root directory.
+    paths_to_headers: A dictionary with keys partial paths and values of header
+    dicts. Key is expected to be a substring of a file being served.
+
+    E.g. if you have a test with files foo.html and foo.js, you can serve them
+    separate headers with the following:
+    paths_to_headers = {
+      'foo.html': {
+        'Content-Security-Policy', "script-src 'unsafe-inline';"
+      },
+      'foo.js': {'Content-Security-Policy', "default-src 'self';"}
+    }
+
+  Returns:
+    A RequestHandler class.
+  """
+
+  class TestDataHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
+    """Handles HTTP requests and serves responses with specified headers.
+
+    For testing purposes only.
+    """
+
+    _current_working_directory = os.getcwd()
+    _base_path = base_path
+    _paths_to_headers = paths_to_headers
+
+    def do_GET(self):  # pylint: disable=invalid-name
+      content = self.get_content()
+      self.wfile.write(content)
+
+    def do_HEAD(self):  # pylint: disable=invalid-name
+      # Run get_content to send the headers, but ignore the returned results
+      self.get_content()
+
+    def translate_path(self, path):
+      """Translate the request path to the file in the testdata directory."""
+
+      potential_path = SimpleHTTPServer.SimpleHTTPRequestHandler.translate_path(
+          self, path)
+      potential_path = potential_path.replace(self._current_working_directory,
+                                              self._base_path)
+      return potential_path
+
+    def get_content(self):
+      """Returns the contents of the file at path with added headers."""
+      path = self.translate_path(self.path)
+      modified_date_time = None
+      raw_content = None
+      try:
+        with open(path, 'rb') as f:
+          filestream = os.fstat(f.fileno())
+          modified_date_time = self.date_time_string(filestream.st_mtime)
+          raw_content = f.read()
+      except IOError:
+        self.send_error(404, 'File not found')
+        return None
+
+      content_type = self.guess_type(path)
+      self.send_response(200)
+      self.send_header('Content-type', content_type)
+      for partial_path, headers in self._paths_to_headers.items():
+        if partial_path in path:
+          for header_key, header_value in headers.items():
+            self.send_header(header_key, header_value)
+
+      content_length = len(raw_content)
+
+      self.send_header('Content-Length', content_length)
+      self.send_header('Last-Modified', modified_date_time)
+      self.end_headers()
+
+      return raw_content
+
+  return TestDataHTTPRequestHandler
+
+
 class ThreadedWebServer(object):
   """A HTTP WebServer that serves requests in a separate thread."""
 
diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc
index 21005ba..5cc3d2d 100644
--- a/cobalt/browser/application.cc
+++ b/cobalt/browser/application.cc
@@ -662,7 +662,7 @@
   // Initializes persistent settings.
   persistent_settings_ =
       std::make_unique<persistent_storage::PersistentSettings>(
-          kPersistentSettingsJson, message_loop_->task_runner());
+          kPersistentSettingsJson);
 
   // Initializes Watchdog.
   watchdog::Watchdog* watchdog =
diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc
index 780f86d..0873200 100644
--- a/cobalt/browser/browser_module.cc
+++ b/cobalt/browser/browser_module.cc
@@ -707,8 +707,8 @@
     const base::Optional<math::Rect>& clip_rect,
     const ScreenShotWriter::ImageEncodeCompleteCallback& screenshot_ready) {
   TRACE_EVENT0("cobalt::browser", "BrowserModule::RequestScreenshotToMemory()");
-  DCHECK_EQ(base::MessageLoop::current(), self_message_loop_);
   DCHECK(screen_shot_writer_);
+  // Note: This does not have to be called from self_message_loop_.
 
   scoped_refptr<render_tree::Node> render_tree;
   web_module_->DoSynchronousLayoutAndGetRenderTree(&render_tree);
diff --git a/cobalt/browser/idl_files.gni b/cobalt/browser/idl_files.gni
index db2d7c5..37256c3 100644
--- a/cobalt/browser/idl_files.gni
+++ b/cobalt/browser/idl_files.gni
@@ -201,6 +201,9 @@
   "//cobalt/subtlecrypto/subtle_crypto.idl",
 
   "//cobalt/web/blob.idl",
+  "//cobalt/web/cache.idl",
+  "//cobalt/web/cache_request.idl",
+  "//cobalt/web/cache_storage.idl",
   "//cobalt/web/cobalt_ua_data_values_interface.idl",
   "//cobalt/web/crypto.idl",
   "//cobalt/web/custom_event.idl",
@@ -341,6 +344,7 @@
   "//cobalt/web/error_event_init.idl",
   "//cobalt/web/event_init.idl",
   "//cobalt/web/message_event_init.idl",
+  "//cobalt/web/multi_cache_query_options.idl",
   "//cobalt/web/navigator_ua_brand_version.idl",
   "//cobalt/web/ua_data_values.idl",
   "//cobalt/web/ua_low_entropy_json.idl",
@@ -405,6 +409,7 @@
   "//cobalt/media_capture/navigator.idl",
   "//cobalt/media_session/navigator_media_session.idl",
   "//cobalt/web/buffer_source.idl",
+  "//cobalt/web/global_cache_storage.idl",
   "//cobalt/web/global_crypto.idl",
   "//cobalt/web/navigator_id.idl",
   "//cobalt/web/navigator_language.idl",
diff --git a/cobalt/browser/web_module.cc b/cobalt/browser/web_module.cc
index a7cfd00..6cb0ea4 100644
--- a/cobalt/browser/web_module.cc
+++ b/cobalt/browser/web_module.cc
@@ -782,10 +782,13 @@
   local_storage_database_.reset();
   mesh_cache_.reset();
   remote_typeface_cache_.reset();
+  // Warning: The image cache may contain animated images that rely on the
+  // thread in AnimatedImageTracker, so it's important to destroy the image
+  // cache before the animated image tracker object.
+  image_cache_.reset();
   animated_image_tracker_.reset();
   dom_parser_.reset();
   css_parser_.reset();
-  image_cache_.reset();
   web_module_stat_tracker_.reset();
 }
 
diff --git a/cobalt/build/build.id b/cobalt/build/build.id
index ce753b0..e77222b 100644
--- a/cobalt/build/build.id
+++ b/cobalt/build/build.id
@@ -1 +1 @@
-309312
\ No newline at end of file
+309559
diff --git a/cobalt/cache/BUILD.gn b/cobalt/cache/BUILD.gn
index edc1daf..237a810 100644
--- a/cobalt/cache/BUILD.gn
+++ b/cobalt/cache/BUILD.gn
@@ -11,6 +11,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+
 static_library("cache") {
   sources = [
     "cache.cc",
@@ -25,5 +26,6 @@
     "//cobalt/persistent_storage:persistent_settings",
     "//net",
     "//starboard:starboard_headers_only",
+    "//starboard/common",
   ]
 }
diff --git a/cobalt/cache/cache.cc b/cobalt/cache/cache.cc
index 867db60..aeb1132 100644
--- a/cobalt/cache/cache.cc
+++ b/cobalt/cache/cache.cc
@@ -27,6 +27,7 @@
 #include "cobalt/extension/javascript_cache.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/disk_cache/cobalt/cobalt_backend_impl.h"
+#include "starboard/common/murmurhash2.h"
 #include "starboard/configuration_constants.h"
 #include "starboard/system.h"
 
@@ -45,6 +46,8 @@
 base::Optional<std::string> GetSubdirectory(
     disk_cache::ResourceType resource_type) {
   switch (resource_type) {
+    case disk_cache::ResourceType::kCacheApi:
+      return "cache_api";
     case disk_cache::ResourceType::kCompiledScript:
       return "compiled_js";
     default:
@@ -89,24 +92,54 @@
   return base::Singleton<Cache, base::LeakySingletonTraits<Cache>>::get();
 }
 
-void Cache::Delete(disk_cache::ResourceType resource_type, uint32_t key) {
-  auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
-  if (memory_capped_directory) {
-    memory_capped_directory->Delete(key);
-  }
+// static
+uint32_t Cache::CreateKey(const std::string& s) {
+  return starboard::MurmurHash2_32(s.c_str(), s.size());
 }
 
-void Cache::DeleteAll() {
-  auto* memory_capped_directory =
-      GetMemoryCappedDirectory(disk_cache::ResourceType::kCompiledScript);
+bool Cache::Delete(disk_cache::ResourceType resource_type, uint32_t key) {
+  auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
+  if (memory_capped_directory) {
+    return memory_capped_directory->Delete(key);
+  }
+  return false;
+}
+
+void Cache::Delete(disk_cache::ResourceType resource_type) {
+  auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
   if (memory_capped_directory) {
     memory_capped_directory->DeleteAll();
   }
 }
 
+void Cache::DeleteAll() {
+  Delete(disk_cache::ResourceType::kCompiledScript);
+  Delete(disk_cache::ResourceType::kCacheApi);
+}
+
+std::vector<uint32_t> Cache::KeysWithMetadata(
+    disk_cache::ResourceType resource_type) {
+  auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
+  if (memory_capped_directory) {
+    return memory_capped_directory->KeysWithMetadata();
+  }
+  return std::vector<uint32_t>();
+}
+
+std::unique_ptr<base::Value> Cache::Metadata(
+    disk_cache::ResourceType resource_type, uint32_t key) {
+  auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
+  if (memory_capped_directory) {
+    return memory_capped_directory->Metadata(key);
+  }
+  return nullptr;
+}
+
 std::unique_ptr<std::vector<uint8_t>> Cache::Retrieve(
     disk_cache::ResourceType resource_type, uint32_t key,
-    std::function<std::unique_ptr<std::vector<uint8_t>>()> generate) {
+    std::function<std::pair<std::unique_ptr<std::vector<uint8_t>>,
+                            base::Optional<base::Value>>()>
+        generate) {
   base::ScopedClosureRunner notifier(base::BindOnce(
       &Cache::Notify, base::Unretained(this), resource_type, key));
   auto* e = GetWaitableEvent(resource_type, key);
@@ -128,12 +161,12 @@
         javascript_cache_extension->ReleaseCachedScriptData(cache_data_buf);
         return data;
       }
-      auto data = generate();
+      auto data = generate().first;
       if (data) {
         javascript_cache_extension->StoreCachedScript(
             key, data->size(), data->data(), data->size());
       }
-      return data;
+      return std::move(data);
     }
   }
 
@@ -145,10 +178,27 @@
     }
   }
   auto data = generate();
-  if (data) {
-    TryStore(resource_type, key, *data);
+  if (data.first) {
+    Store(resource_type, key, /*data=*/*(data.first), /*metadata=*/data.second);
   }
-  return data;
+  return std::move(data.first);
+}
+
+std::unique_ptr<std::vector<uint8_t>> Cache::Retrieve(
+    disk_cache::ResourceType resource_type, uint32_t key) {
+  base::ScopedClosureRunner notifier(base::BindOnce(
+      &Cache::Notify, base::Unretained(this), resource_type, key));
+  auto* e = GetWaitableEvent(resource_type, key);
+  if (e) {
+    e->Wait();
+    delete e;
+  }
+
+  auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
+  if (memory_capped_directory) {
+    return memory_capped_directory->Retrieve(key);
+  }
+  return nullptr;
 }
 
 void Cache::set_enabled(bool enabled) { enabled_ = enabled; }
@@ -196,7 +246,9 @@
 }
 
 void Cache::Resize(disk_cache::ResourceType resource_type, uint32_t bytes) {
-  if (resource_type != disk_cache::ResourceType::kCompiledScript) return;
+  if (resource_type != disk_cache::ResourceType::kCacheApi &&
+      resource_type != disk_cache::ResourceType::kCompiledScript)
+    return;
   if (bytes == disk_cache::kTypeMetadata[resource_type].max_size_bytes) return;
 
   if (persistent_settings_) {
@@ -209,11 +261,15 @@
   if (memory_capped_directory) {
     memory_capped_directory->Resize(bytes);
   }
+  if (bytes == 0) {
+    Delete(resource_type);
+  }
 }
 
 base::Optional<uint32_t> Cache::GetMaxCacheStorageInBytes(
     disk_cache::ResourceType resource_type) {
   switch (resource_type) {
+    case disk_cache::ResourceType::kCacheApi:
     case disk_cache::ResourceType::kCompiledScript:
       return disk_cache::kTypeMetadata[resource_type].max_size_bytes;
     default:
@@ -252,14 +308,15 @@
   pending_[resource_type].erase(key);
 }
 
-void Cache::TryStore(disk_cache::ResourceType resource_type, uint32_t key,
-                     const std::vector<uint8_t>& data) {
+void Cache::Store(disk_cache::ResourceType resource_type, uint32_t key,
+                  const std::vector<uint8_t>& data,
+                  const base::Optional<base::Value>& metadata) {
   if (!CanCache(resource_type, data.size())) {
     return;
   }
   auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type);
   if (memory_capped_directory) {
-    memory_capped_directory->Store(key, data);
+    memory_capped_directory->Store(key, data, metadata);
   }
 }
 
diff --git a/cobalt/cache/cache.h b/cobalt/cache/cache.h
index fd106f1..5b57be6 100644
--- a/cobalt/cache/cache.h
+++ b/cobalt/cache/cache.h
@@ -18,14 +18,18 @@
 #include <functional>
 #include <map>
 #include <memory>
+#include <string>
+#include <utility>
 #include <vector>
 
 #include "base/basictypes.h"
 #include "base/files/file_enumerator.h"
 #include "base/files/file_path.h"
 #include "base/macros.h"
+#include "base/optional.h"
 #include "base/synchronization/lock.h"
 #include "base/synchronization/waitable_event.h"
+#include "base/values.h"
 #include "cobalt/cache/memory_capped_directory.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "net/disk_cache/cobalt/resource_type.h"
@@ -41,12 +45,26 @@
 class Cache {
  public:
   static Cache* GetInstance();
-  void Delete(disk_cache::ResourceType resource_type, uint32_t key);
+  static uint32_t CreateKey(const std::string& s);
+
+  bool Delete(disk_cache::ResourceType resource_type, uint32_t key);
+  void Delete(disk_cache::ResourceType resource_type);
   void DeleteAll();
+  std::vector<uint32_t> KeysWithMetadata(
+      disk_cache::ResourceType resource_type);
+  std::unique_ptr<base::Value> Metadata(disk_cache::ResourceType resource_type,
+                                        uint32_t key);
   std::unique_ptr<std::vector<uint8_t>> Retrieve(
       disk_cache::ResourceType resource_type, uint32_t key,
-      std::function<std::unique_ptr<std::vector<uint8_t>>()> generate);
+      std::function<std::pair<std::unique_ptr<std::vector<uint8_t>>,
+                              base::Optional<base::Value>>()>
+          generate);
+  std::unique_ptr<std::vector<uint8_t>> Retrieve(
+      disk_cache::ResourceType resource_type, uint32_t key);
   void Resize(disk_cache::ResourceType resource_type, uint32_t bytes);
+  void Store(disk_cache::ResourceType resource_type, uint32_t key,
+             const std::vector<uint8_t>& data,
+             const base::Optional<base::Value>& metadata);
   base::Optional<uint32_t> GetMaxCacheStorageInBytes(
       disk_cache::ResourceType resource_type);
 
@@ -64,8 +82,6 @@
   base::WaitableEvent* GetWaitableEvent(disk_cache::ResourceType resource_type,
                                         uint32_t key);
   void Notify(disk_cache::ResourceType resource_type, uint32_t key);
-  void TryStore(disk_cache::ResourceType resource_type, uint32_t key,
-                const std::vector<uint8_t>& data);
   bool CanCache(disk_cache::ResourceType resource_type, uint32_t data_size);
 
   mutable base::Lock lock_;
diff --git a/cobalt/cache/memory_capped_directory.cc b/cobalt/cache/memory_capped_directory.cc
index 3dc064c..de70439 100644
--- a/cobalt/cache/memory_capped_directory.cc
+++ b/cobalt/cache/memory_capped_directory.cc
@@ -15,14 +15,23 @@
 #include "cobalt/cache/memory_capped_directory.h"
 
 #include <algorithm>
+#include <string>
 
 #include "base/files/file_util.h"
+#include "base/json/json_reader.h"
+#include "base/json/json_writer.h"
 #include "base/strings/string_number_conversions.h"
 #include "starboard/directory.h"
 
 namespace cobalt {
 namespace cache {
 
+namespace {
+
+const char* kMetadataExtension = ".json";
+
+}  // namespace
+
 MemoryCappedDirectory::FileInfo::FileInfo(
     base::FilePath directory_path, base::FileEnumerator::FileInfo file_info)
     : file_path_(directory_path.Append(file_info.GetName())),
@@ -58,6 +67,13 @@
   while (!file_enumerator.Next().empty()) {
     MemoryCappedDirectory::FileInfo file_info(directory_path,
                                               file_enumerator.GetInfo());
+    // TODO: include metadata size in file sizes.
+    if (file_info.file_path_.Extension() == kMetadataExtension) {
+      memory_capped_directory->file_keys_with_metadata_[file_info.file_path_] =
+          std::stoul(
+              file_info.file_path_.BaseName().RemoveExtension().MaybeAsASCII());
+      continue;
+    }
     heap->push_back(file_info);
     memory_capped_directory->file_sizes_[file_info.file_path_] =
         file_info.size_;
@@ -70,13 +86,18 @@
   return memory_capped_directory;
 }
 
-void MemoryCappedDirectory::Delete(uint32_t key) {
+bool MemoryCappedDirectory::Delete(uint32_t key) {
   base::AutoLock auto_lock(lock_);
   auto file_path = GetFilePath(key);
   if (base::PathExists(file_path)) {
     base::DeleteFile(file_path, false);
   }
+  auto metadata_path = file_path.AddExtension(kMetadataExtension);
+  if (base::PathExists(metadata_path)) {
+    base::DeleteFile(metadata_path, false);
+  }
   file_sizes_.erase(file_path);
+  file_keys_with_metadata_.erase(file_path);
   auto* heap = &file_info_heap_;
   for (auto it = heap->begin(); it != heap->end(); ++it) {
     if (it->file_path_ == file_path) {
@@ -86,9 +107,10 @@
         std::make_heap(heap->begin(), heap->end(),
                        MemoryCappedDirectory::FileInfo::OldestFirst());
       }
-      return;
+      return true;
     }
   }
+  return false;
 }
 
 void MemoryCappedDirectory::DeleteAll() {
@@ -99,9 +121,29 @@
   SbDirectoryCreate(directory_path_.value().c_str());
   file_info_heap_.clear();
   file_sizes_.clear();
+  file_keys_with_metadata_.clear();
   size_ = 0;
 }
 
+std::vector<uint32_t> MemoryCappedDirectory::KeysWithMetadata() {
+  std::vector<uint32_t> keys(file_keys_with_metadata_.size());
+  for (auto it = file_keys_with_metadata_.begin();
+       it != file_keys_with_metadata_.end(); ++it) {
+    keys.push_back(it->second);
+  }
+  return keys;
+}
+
+std::unique_ptr<base::Value> MemoryCappedDirectory::Metadata(uint32_t key) {
+  auto metadata_path = GetFilePath(key).AddExtension(kMetadataExtension);
+  if (!base::PathExists(metadata_path)) {
+    return nullptr;
+  }
+  std::string serialized_metadata;
+  base::ReadFileToString(metadata_path, &serialized_metadata);
+  return base::JSONReader::Read(serialized_metadata);
+}
+
 std::unique_ptr<std::vector<uint8_t>> MemoryCappedDirectory::Retrieve(
     uint32_t key) {
   auto file_path = GetFilePath(key);
@@ -109,6 +151,10 @@
   if (it == file_sizes_.end()) {
     return nullptr;
   }
+  if (!base::PathExists(file_path)) {
+    Delete(key);
+    return nullptr;
+  }
   auto size = it->second;
   auto data = std::make_unique<std::vector<uint8_t>>(static_cast<size_t>(size));
   int bytes_read = base::ReadFile(
@@ -120,17 +166,27 @@
 }
 
 void MemoryCappedDirectory::Store(uint32_t key,
-                                  const std::vector<uint8_t>& data) {
+                                  const std::vector<uint8_t>& data,
+                                  const base::Optional<base::Value>& metadata) {
   base::AutoLock auto_lock(lock_);
   auto file_path = GetFilePath(key);
   uint32_t new_entry_size = static_cast<uint32_t>(data.size());
   if (!EnsureEnoughSpace(new_entry_size)) {
     return;
   }
+  if (metadata) {
+    std::string serialized_metadata;
+    base::JSONWriter::Write(metadata.value(), &serialized_metadata);
+    base::WriteFile(file_path.AddExtension(kMetadataExtension),
+                    serialized_metadata.data(), serialized_metadata.size());
+  }
   int bytes_written = base::WriteFile(
       file_path, reinterpret_cast<const char*>(data.data()), data.size());
   if (bytes_written != data.size()) {
     base::DeleteFile(file_path, false);
+    if (metadata) {
+      base::DeleteFile(file_path.AddExtension(kMetadataExtension), false);
+    }
     return;
   }
   size_ += new_entry_size;
@@ -140,6 +196,7 @@
   std::push_heap(heap->begin(), heap->end(),
                  MemoryCappedDirectory::FileInfo::OldestFirst());
   file_sizes_[file_path] = new_entry_size;
+  file_keys_with_metadata_[file_path] = key;
 }
 
 void MemoryCappedDirectory::Resize(uint32_t size) {
@@ -173,7 +230,12 @@
     auto removed = heap->back();
     size_ -= removed.size_;
     base::DeleteFile(removed.file_path_, false);
+    auto metadata_path = removed.file_path_.AddExtension(kMetadataExtension);
+    if (base::PathExists(metadata_path)) {
+      base::DeleteFile(metadata_path, false);
+    }
     file_sizes_.erase(removed.file_path_);
+    file_keys_with_metadata_.erase(removed.file_path_);
     heap->pop_back();
   }
   return true;
diff --git a/cobalt/cache/memory_capped_directory.h b/cobalt/cache/memory_capped_directory.h
index 941ab19..39d227d 100644
--- a/cobalt/cache/memory_capped_directory.h
+++ b/cobalt/cache/memory_capped_directory.h
@@ -25,6 +25,7 @@
 #include "base/macros.h"
 #include "base/optional.h"
 #include "base/synchronization/lock.h"
+#include "base/values.h"
 
 namespace cobalt {
 namespace cache {
@@ -50,10 +51,13 @@
  public:
   static std::unique_ptr<MemoryCappedDirectory> Create(
       const base::FilePath& directory_path, uint32_t max_size);
-  void Delete(uint32_t key);
+  bool Delete(uint32_t key);
   void DeleteAll();
+  std::vector<uint32_t> KeysWithMetadata();
+  std::unique_ptr<base::Value> Metadata(uint32_t key);
   std::unique_ptr<std::vector<uint8_t>> Retrieve(uint32_t key);
-  void Store(uint32_t key, const std::vector<uint8_t>& data);
+  void Store(uint32_t key, const std::vector<uint8_t>& data,
+             const base::Optional<base::Value>& metadata);
   void Resize(uint32_t size);
 
  private:
@@ -68,6 +72,7 @@
   // Min-heap determined by last modified time.
   std::vector<FileInfo> file_info_heap_;
   std::map<base::FilePath, uint32_t> file_sizes_;
+  std::map<base::FilePath, uint32_t> file_keys_with_metadata_;
   uint32_t size_;
   uint32_t max_size_;
 
diff --git a/cobalt/content/licenses/platform/android/licenses_cobalt.txt b/cobalt/content/licenses/platform/android/licenses_cobalt.txt
index 022c76b..6754136 100644
--- a/cobalt/content/licenses/platform/android/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/android/licenses_cobalt.txt
@@ -1884,87 +1884,6 @@
 
 
 
-  libjpeg
-
-
-  (Copied from the README.)
-
-  --------------------------------------------------------------------------------
-
-  The authors make NO WARRANTY or representation, either express or implied,
-  with respect to this software, its quality, accuracy, merchantability, or
-  fitness for a particular purpose.  This software is provided "AS IS", and you,
-  its user, assume the entire risk as to its quality and accuracy.
-
-  This software is copyright (C) 1991-1998, Thomas G. Lane.
-  All Rights Reserved except as specified below.
-
-  Permission is hereby granted to use, copy, modify, and distribute this
-  software (or portions thereof) for any purpose, without fee, subject to these
-  conditions:
-  (1) If any part of the source code for this software is distributed, then this
-  README file must be included, with this copyright and no-warranty notice
-  unaltered; and any additions, deletions, or changes to the original files
-  must be clearly indicated in accompanying documentation.
-  (2) If only executable code is distributed, then the accompanying
-  documentation must state that "this software is based in part on the work of
-  the Independent JPEG Group".
-  (3) Permission for use of this software is granted only if the user accepts
-  full responsibility for any undesirable consequences; the authors accept
-  NO LIABILITY for damages of any kind.
-
-  These conditions apply to any software derived from or based on the IJG code,
-  not just to the unmodified library.  If you use our work, you ought to
-  acknowledge us.
-
-  Permission is NOT granted for the use of any IJG author's name or company name
-  in advertising or publicity relating to this software or products derived from
-  it.  This software may be referred to only as "the Independent JPEG Group's
-  software".
-
-  We specifically permit and encourage the use of this software as the basis of
-  commercial products, provided that all warranty or liability claims are
-  assumed by the product vendor.
-
-
-  ansi2knr.c is included in this distribution by permission of L. Peter Deutsch,
-  sole proprietor of its copyright holder, Aladdin Enterprises of Menlo Park, CA.
-  ansi2knr.c is NOT covered by the above copyright and conditions, but instead
-  by the usual distribution terms of the Free Software Foundation; principally,
-  that you must include source code if you redistribute it.  (See the file
-  ansi2knr.c for full details.)  However, since ansi2knr.c is not needed as part
-  of any program generated from the IJG code, this does not limit you more than
-  the foregoing paragraphs do.
-
-  The Unix configuration script "configure" was produced with GNU Autoconf.
-  It is copyright by the Free Software Foundation but is freely distributable.
-  The same holds for its supporting scripts (config.guess, config.sub,
-  ltconfig, ltmain.sh).  Another support script, install-sh, is copyright
-  by M.I.T. but is also freely distributable.
-
-  It appears that the arithmetic coding option of the JPEG spec is covered by
-  patents owned by IBM, AT&T, and Mitsubishi.  Hence arithmetic coding cannot
-  legally be used without obtaining one or more licenses.  For this reason,
-  support for arithmetic coding has been removed from the free JPEG software.
-  (Since arithmetic coding provides only a marginal gain over the unpatented
-  Huffman mode, it is unlikely that very many implementations will support it.)
-  So far as we are aware, there are no patent restrictions on the remaining
-  code.
-
-  The IJG distribution formerly included code to read and write GIF files.
-  To avoid entanglement with the Unisys LZW patent, GIF reading support has
-  been removed altogether, and the GIF writer has been simplified to produce
-  "uncompressed GIFs".  This technique does not use the LZW algorithm; the
-  resulting GIF files are larger than usual, but are readable by all standard
-  GIF decoders.
-
-  We are required to state that
-      "The Graphics Interchange Format(c) is the Copyright property of
-      CompuServe Incorporated.  GIF(sm) is a Service Mark property of
-      CompuServe Incorporated."
-
-
-
   libpng
 
 
diff --git a/cobalt/content/licenses/platform/default/licenses_cobalt.txt b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
index 7ab54cf..fd93a89 100644
--- a/cobalt/content/licenses/platform/default/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/default/licenses_cobalt.txt
@@ -2064,86 +2064,6 @@
 
 
 
-  libjpeg
-
-
-  (Copied from the README.)
-
-  --------------------------------------------------------------------------------
-
-  The authors make NO WARRANTY or representation, either express or implied,
-  with respect to this software, its quality, accuracy, merchantability, or
-  fitness for a particular purpose.  This software is provided "AS IS", and you,
-  its user, assume the entire risk as to its quality and accuracy.
-
-  This software is copyright (C) 1991-1998, Thomas G. Lane.
-  All Rights Reserved except as specified below.
-
-  Permission is hereby granted to use, copy, modify, and distribute this
-  software (or portions thereof) for any purpose, without fee, subject to these
-  conditions:
-  (1) If any part of the source code for this software is distributed, then this
-  README file must be included, with this copyright and no-warranty notice
-  unaltered; and any additions, deletions, or changes to the original files
-  must be clearly indicated in accompanying documentation.
-  (2) If only executable code is distributed, then the accompanying
-  documentation must state that "this software is based in part on the work of
-  the Independent JPEG Group".
-  (3) Permission for use of this software is granted only if the user accepts
-  full responsibility for any undesirable consequences; the authors accept
-  NO LIABILITY for damages of any kind.
-
-  These conditions apply to any software derived from or based on the IJG code,
-  not just to the unmodified library.  If you use our work, you ought to
-  acknowledge us.
-
-  Permission is NOT granted for the use of any IJG author's name or company name
-  in advertising or publicity relating to this software or products derived from
-  it.  This software may be referred to only as "the Independent JPEG Group's
-  software".
-
-  We specifically permit and encourage the use of this software as the basis of
-  commercial products, provided that all warranty or liability claims are
-  assumed by the product vendor.
-
-
-  ansi2knr.c is included in this distribution by permission of L. Peter Deutsch,
-  sole proprietor of its copyright holder, Aladdin Enterprises of Menlo Park, CA.
-  ansi2knr.c is NOT covered by the above copyright and conditions, but instead
-  by the usual distribution terms of the Free Software Foundation; principally,
-  that you must include source code if you redistribute it.  (See the file
-  ansi2knr.c for full details.)  However, since ansi2knr.c is not needed as part
-  of any program generated from the IJG code, this does not limit you more than
-  the foregoing paragraphs do.
-
-  The Unix configuration script "configure" was produced with GNU Autoconf.
-  It is copyright by the Free Software Foundation but is freely distributable.
-  The same holds for its supporting scripts (config.guess, config.sub,
-  ltconfig, ltmain.sh).  Another support script, install-sh, is copyright
-  by M.I.T. but is also freely distributable.
-
-  It appears that the arithmetic coding option of the JPEG spec is covered by
-  patents owned by IBM, AT&T, and Mitsubishi.  Hence arithmetic coding cannot
-  legally be used without obtaining one or more licenses.  For this reason,
-  support for arithmetic coding has been removed from the free JPEG software.
-  (Since arithmetic coding provides only a marginal gain over the unpatented
-  Huffman mode, it is unlikely that very many implementations will support it.)
-  So far as we are aware, there are no patent restrictions on the remaining
-  code.
-
-  The IJG distribution formerly included code to read and write GIF files.
-  To avoid entanglement with the Unisys LZW patent, GIF reading support has
-  been removed altogether, and the GIF writer has been simplified to produce
-  "uncompressed GIFs".  This technique does not use the LZW algorithm; the
-  resulting GIF files are larger than usual, but are readable by all standard
-  GIF decoders.
-
-  We are required to state that
-      "The Graphics Interchange Format(c) is the Copyright property of
-      CompuServe Incorporated.  GIF(sm) is a Service Mark property of
-      CompuServe Incorporated."
-
-
   libjpeg-turbo
 
   libjpeg-turbo Licenses libjpeg-turbo is covered by three compatible BSD-style
diff --git a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
index a85d482..8063535 100644
--- a/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
+++ b/cobalt/content/licenses/platform/evergreen/licenses_cobalt.txt
@@ -2064,87 +2064,6 @@
 
 
 
-  libjpeg
-
-
-  (Copied from the README.)
-
-  --------------------------------------------------------------------------------
-
-  The authors make NO WARRANTY or representation, either express or implied,
-  with respect to this software, its quality, accuracy, merchantability, or
-  fitness for a particular purpose.  This software is provided "AS IS", and you,
-  its user, assume the entire risk as to its quality and accuracy.
-
-  This software is copyright (C) 1991-1998, Thomas G. Lane.
-  All Rights Reserved except as specified below.
-
-  Permission is hereby granted to use, copy, modify, and distribute this
-  software (or portions thereof) for any purpose, without fee, subject to these
-  conditions:
-  (1) If any part of the source code for this software is distributed, then this
-  README file must be included, with this copyright and no-warranty notice
-  unaltered; and any additions, deletions, or changes to the original files
-  must be clearly indicated in accompanying documentation.
-  (2) If only executable code is distributed, then the accompanying
-  documentation must state that "this software is based in part on the work of
-  the Independent JPEG Group".
-  (3) Permission for use of this software is granted only if the user accepts
-  full responsibility for any undesirable consequences; the authors accept
-  NO LIABILITY for damages of any kind.
-
-  These conditions apply to any software derived from or based on the IJG code,
-  not just to the unmodified library.  If you use our work, you ought to
-  acknowledge us.
-
-  Permission is NOT granted for the use of any IJG author's name or company name
-  in advertising or publicity relating to this software or products derived from
-  it.  This software may be referred to only as "the Independent JPEG Group's
-  software".
-
-  We specifically permit and encourage the use of this software as the basis of
-  commercial products, provided that all warranty or liability claims are
-  assumed by the product vendor.
-
-
-  ansi2knr.c is included in this distribution by permission of L. Peter Deutsch,
-  sole proprietor of its copyright holder, Aladdin Enterprises of Menlo Park, CA.
-  ansi2knr.c is NOT covered by the above copyright and conditions, but instead
-  by the usual distribution terms of the Free Software Foundation; principally,
-  that you must include source code if you redistribute it.  (See the file
-  ansi2knr.c for full details.)  However, since ansi2knr.c is not needed as part
-  of any program generated from the IJG code, this does not limit you more than
-  the foregoing paragraphs do.
-
-  The Unix configuration script "configure" was produced with GNU Autoconf.
-  It is copyright by the Free Software Foundation but is freely distributable.
-  The same holds for its supporting scripts (config.guess, config.sub,
-  ltconfig, ltmain.sh).  Another support script, install-sh, is copyright
-  by M.I.T. but is also freely distributable.
-
-  It appears that the arithmetic coding option of the JPEG spec is covered by
-  patents owned by IBM, AT&T, and Mitsubishi.  Hence arithmetic coding cannot
-  legally be used without obtaining one or more licenses.  For this reason,
-  support for arithmetic coding has been removed from the free JPEG software.
-  (Since arithmetic coding provides only a marginal gain over the unpatented
-  Huffman mode, it is unlikely that very many implementations will support it.)
-  So far as we are aware, there are no patent restrictions on the remaining
-  code.
-
-  The IJG distribution formerly included code to read and write GIF files.
-  To avoid entanglement with the Unisys LZW patent, GIF reading support has
-  been removed altogether, and the GIF writer has been simplified to produce
-  "uncompressed GIFs".  This technique does not use the LZW algorithm; the
-  resulting GIF files are larger than usual, but are readable by all standard
-  GIF decoders.
-
-  We are required to state that
-      "The Graphics Interchange Format(c) is the Copyright property of
-      CompuServe Incorporated.  GIF(sm) is a Service Mark property of
-      CompuServe Incorporated."
-
-
-
   libpng
 
 
diff --git a/cobalt/css_parser/parser.cc b/cobalt/css_parser/parser.cc
index f0903f4..49a1e2d 100644
--- a/cobalt/css_parser/parser.cc
+++ b/cobalt/css_parser/parser.cc
@@ -581,8 +581,6 @@
       message_verbosity_(message_verbosity),
       supports_map_to_mesh_(supports_map_to_mesh) {}
 
-Parser::~Parser() {}
-
 scoped_refptr<cssom::CSSStyleSheet> Parser::ParseStyleSheet(
     const std::string& input, const ::base::SourceLocation& input_location) {
   ParserImpl parser_impl(input, input_location, this, on_warning_callback_,
diff --git a/cobalt/css_parser/parser.h b/cobalt/css_parser/parser.h
index 7740788..9b96fe8 100644
--- a/cobalt/css_parser/parser.h
+++ b/cobalt/css_parser/parser.h
@@ -44,7 +44,7 @@
   // logging or map-to-mesh.
   static std::unique_ptr<Parser> Create();
 
-  ~Parser();
+  ~Parser() override {}
 
   scoped_refptr<cssom::CSSStyleSheet> ParseStyleSheet(
       const std::string& input,
@@ -88,7 +88,7 @@
     kShort,
 
     // With message verbosity set to kVerbose, expect to see information such
-    // as the line that caused the error to be output, allong with an arrow
+    // as the line that caused the error to be output, along with an arrow
     // pointing to where the error occurred on the line.
     kVerbose,
   };
diff --git a/cobalt/cssom/computed_style.cc b/cobalt/cssom/computed_style.cc
index 480e437..9f9212d 100644
--- a/cobalt/cssom/computed_style.cc
+++ b/cobalt/cssom/computed_style.cc
@@ -15,6 +15,7 @@
 #include "cobalt/cssom/computed_style.h"
 
 #include <memory>
+#include <utility>
 #include <vector>
 
 #include "cobalt/base/polymorphic_downcast.h"
@@ -1445,8 +1446,7 @@
     }
   }
 
-  void VisitDefault(PropertyValue* property_value) override {
-  }
+  void VisitDefault(PropertyValue* property_value) override {}
 
   bool computed_length_is_negative() { return computed_length_is_negative_; }
 
@@ -2526,14 +2526,14 @@
 
 void ComputedTransformFunctionProvider::VisitCobaltUiNavFocusTransform(
     const CobaltUiNavFocusTransformFunction* focus_function) {
-  computed_transform_function_.reset(new CobaltUiNavFocusTransformFunction(
-      *focus_function));
+  computed_transform_function_.reset(
+      new CobaltUiNavFocusTransformFunction(*focus_function));
 }
 
 void ComputedTransformFunctionProvider::VisitCobaltUiNavSpotlightTransform(
     const CobaltUiNavSpotlightTransformFunction* spotlight_function) {
-  computed_transform_function_.reset(new CobaltUiNavSpotlightTransformFunction(
-      *spotlight_function));
+  computed_transform_function_.reset(
+      new CobaltUiNavSpotlightTransformFunction(*spotlight_function));
 }
 
 // Absolutizes the value of "text-indent" property.
@@ -2679,7 +2679,7 @@
       base::polymorphic_downcast<TransformFunctionListValue*>(
           transform_property_value);
   if (!transform_function_list->value().HasTrait(
-      TransformFunction::kTraitUsesRelativeUnits)) {
+          TransformFunction::kTraitUsesRelativeUnits)) {
     // If the transform list contains no transforms that use relative units,
     // then we do not need to do anything and we can pass through the existing
     // transform.
@@ -3496,10 +3496,13 @@
   DCHECK(cascaded_style);
   DCHECK(parent_computed_style_declaration);
   DCHECK(root_computed_style);
-
-  // Create a context for calculating the computed style.  This object is useful
-  // because it can cache computed style values that are depended upon by other
-  // properties' computed style calculations.
+  if (!cascaded_style || !root_computed_style ||
+      !parent_computed_style_declaration) {
+    return;
+  }
+  // Create a context for calculating the computed style.  This object is
+  // useful because it can cache computed style values that are depended upon
+  // by other properties' computed style calculations.
   CalculateComputedStyleContext calculate_computed_style_context(
       cascaded_style.get(), parent_computed_style_declaration,
       root_computed_style, viewport_size, property_key_to_base_url_map);
diff --git a/cobalt/cssom/css_parser.h b/cobalt/cssom/css_parser.h
index d56809b..b1e7e61 100644
--- a/cobalt/cssom/css_parser.h
+++ b/cobalt/cssom/css_parser.h
@@ -41,6 +41,7 @@
 // This class is not a part of any specification.
 class CSSParser {
  public:
+  virtual ~CSSParser() {}
   //
   // Parser entry points
   // (see http://dev.w3.org/csswg/css-syntax/#parser-entry-points):
@@ -102,9 +103,6 @@
   virtual scoped_refptr<MediaQuery> ParseMediaQuery(
       const std::string& media_query,
       const base::SourceLocation& input_location) = 0;
-
- protected:
-  virtual ~CSSParser() {}
 };
 
 }  // namespace cssom
diff --git a/cobalt/cssom/user_agent_style_sheet.cc b/cobalt/cssom/user_agent_style_sheet.cc
index cdb4164..f0c3876 100644
--- a/cobalt/cssom/user_agent_style_sheet.cc
+++ b/cobalt/cssom/user_agent_style_sheet.cc
@@ -22,6 +22,9 @@
 namespace cssom {
 
 scoped_refptr<CSSStyleSheet> ParseUserAgentStyleSheet(CSSParser* css_parser) {
+  if (!css_parser) {
+    return nullptr;
+  }
   const char kUserAgentStyleSheetFileName[] = "user_agent_style_sheet.css";
 
   // Parse the user agent style sheet from the given file that was compiled
@@ -41,8 +44,10 @@
               reinterpret_cast<const char*>(html_css_file_contents.data),
               static_cast<size_t>(html_css_file_contents.size)),
           base::SourceLocation(kUserAgentStyleSheetFileName, 1, 1));
-  user_agent_style_sheet->set_origin(kNormalUserAgent);
-  user_agent_style_sheet->SetOriginClean(true);
+  if (user_agent_style_sheet) {
+    user_agent_style_sheet->set_origin(kNormalUserAgent);
+    user_agent_style_sheet->SetOriginClean(true);
+  }
   return user_agent_style_sheet;
 }
 
diff --git a/cobalt/dom/abort_controller.idl b/cobalt/dom/abort_controller.idl
index 36ebebd..0280347 100644
--- a/cobalt/dom/abort_controller.idl
+++ b/cobalt/dom/abort_controller.idl
@@ -15,6 +15,7 @@
 // https://dom.spec.whatwg.org/#interface-abortcontroller
 
 [
+  Exposed=(Window,Worker),
   Constructor,
   ConstructorCallWith=EnvironmentSettings,
 ]
diff --git a/cobalt/dom/comment_test.cc b/cobalt/dom/comment_test.cc
index b68580d..0fb5554 100644
--- a/cobalt/dom/comment_test.cc
+++ b/cobalt/dom/comment_test.cc
@@ -14,10 +14,15 @@
 
 #include "cobalt/dom/comment.h"
 
+#include <memory>
+
 #include "cobalt/dom/document.h"
+#include "cobalt/dom/dom_stat_tracker.h"
 #include "cobalt/dom/element.h"
 #include "cobalt/dom/global_stats.h"
 #include "cobalt/dom/html_element_context.h"
+#include "cobalt/dom/testing/fake_document.h"
+#include "cobalt/dom/testing/stub_window.h"
 #include "cobalt/dom/text.h"
 #include "cobalt/web/testing/gtest_workarounds.h"
 #include "testing/gtest/include/gtest/gtest.h"
@@ -34,18 +39,27 @@
   CommentTest();
   ~CommentTest() override;
 
-  base::MessageLoop message_loop_;
-  HTMLElementContext html_element_context_;
+  std::unique_ptr<testing::StubWindow> window_;
+  std::unique_ptr<HTMLElementContext> html_element_context_;
+  std::unique_ptr<DomStatTracker> dom_stat_tracker_;
   scoped_refptr<Document> document_;
 };
 
-CommentTest::CommentTest() {
+CommentTest::CommentTest()
+    : window_(new testing::StubWindow),
+      dom_stat_tracker_(new DomStatTracker("CommentTest")) {
   EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
-  document_ = new Document(&html_element_context_);
+  window_->InitializeWindow();
+  html_element_context_.reset(new HTMLElementContext(
+      window_->web_context()->environment_settings(), NULL, NULL, NULL, NULL,
+      NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+      dom_stat_tracker_.get(), "", base::kApplicationStateStarted, NULL, NULL));
+  document_ = new testing::FakeDocument(html_element_context_.get());
 }
 
 CommentTest::~CommentTest() {
-  document_ = NULL;
+  window_.reset();
+  document_ = nullptr;
   EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
 }
 
diff --git a/cobalt/dom/document.cc b/cobalt/dom/document.cc
index 41bfa41a..c35472c 100644
--- a/cobalt/dom/document.cc
+++ b/cobalt/dom/document.cc
@@ -82,12 +82,11 @@
 }  // namespace
 
 Document::Document(HTMLElementContext* html_element_context,
-                   const Options& options)
+                   const Options& options, web::CspDelegate* csp_delegate)
     : ALLOW_THIS_IN_INITIALIZER_LIST(Node(html_element_context, this)),
       html_element_context_(html_element_context),
       application_lifecycle_state_(
           html_element_context_->application_lifecycle_state()),
-      window_(options.window),
       implementation_(new DOMImplementation(html_element_context)),
       style_sheets_(new cssom::StyleSheetList()),
       loading_counter_(0),
@@ -124,9 +123,13 @@
 
   cookie_jar_ = options.cookie_jar;
 
+  if (!csp_delegate) {
+    csp_delegate = Document::GetCSPDelegate();
+  }
+
   location_ = new Location(
       options.url, options.hashchange_callback, options.navigation_callback,
-      CreateSecurityCallback(csp_delegate(), web::CspDelegate::kLocation),
+      CreateSecurityCallback(csp_delegate, web::CspDelegate::kLocation),
       base::Bind(&Document::SetNavigationType, base::Unretained(this)));
 
   font_cache_.reset(new FontCache(
@@ -135,18 +138,16 @@
       base::Bind(&Document::OnTypefaceLoadEvent, base::Unretained(this)),
       html_element_context_->font_language_script(), location_));
 
-  if (HasBrowsingContext()) {
-    if (html_element_context_->remote_typeface_cache()) {
-      html_element_context_->remote_typeface_cache()->set_security_callback(
-          CreateSecurityCallback(csp_delegate(), web::CspDelegate::kFont));
-    }
-    if (html_element_context_->image_cache()) {
-      html_element_context_->image_cache()->set_security_callback(
-          CreateSecurityCallback(csp_delegate(), web::CspDelegate::kImage));
-    }
-
-    ready_state_ = kDocumentReadyStateLoading;
+  if (html_element_context_->remote_typeface_cache()) {
+    html_element_context_->remote_typeface_cache()->set_security_callback(
+        CreateSecurityCallback(csp_delegate, web::CspDelegate::kFont));
   }
+  if (html_element_context_->image_cache()) {
+    html_element_context_->image_cache()->set_security_callback(
+        CreateSecurityCallback(csp_delegate, web::CspDelegate::kImage));
+  }
+
+  ready_state_ = kDocumentReadyStateLoading;
 
   // Sample the timeline upon initialization.
   SampleTimelineTime();
@@ -164,7 +165,7 @@
   return first_element_child();
 }
 
-scoped_refptr<Window> Document::default_view() const { return window_; }
+scoped_refptr<Window> Document::default_view() const { return window(); }
 
 std::string Document::title() const {
   const char kTitleTag[] = "title";
@@ -571,7 +572,25 @@
   }
 }
 
-const scoped_refptr<Window> Document::window() { return window_; }
+web::CspDelegate* Document::GetCSPDelegate() const {
+  web::WindowOrWorkerGlobalScope* window_or_worker_global_scope =
+      environment_settings()->context()
+          ? environment_settings()->context()->GetWindowOrWorkerGlobalScope()
+          : nullptr;
+  return window_or_worker_global_scope
+             ? window_or_worker_global_scope->csp_delegate()
+             : nullptr;
+}
+
+const scoped_refptr<Window> Document::window() const {
+  web::WindowOrWorkerGlobalScope* window_or_worker_global_scope =
+      environment_settings()->context()
+          ? environment_settings()->context()->GetWindowOrWorkerGlobalScope()
+          : nullptr;
+  return window_or_worker_global_scope
+             ? window_or_worker_global_scope->AsWindow()
+             : nullptr;
+}
 
 void Document::IncreaseLoadingCounter() { ++loading_counter_; }
 
@@ -637,7 +656,7 @@
 
 void Document::NotifyUrlChanged(const GURL& url) {
   location_->set_url(url);
-  csp_delegate()->NotifyUrlChanged(url);
+  GetCSPDelegate()->NotifyUrlChanged(url);
 }
 
 void Document::OnFocusChange() {
@@ -1011,10 +1030,7 @@
 }
 
 void Document::DisableJit() {
-  window_->html_element_context()
-      ->script_runner()
-      ->GetGlobalEnvironment()
-      ->DisableJit();
+  environment_settings()->context()->global_environment()->DisableJit();
 }
 
 void Document::OnWindowFocusChanged(bool has_focus) {
@@ -1212,7 +1228,7 @@
 }
 
 void Document::OnRootElementUnableToProvideOffsetDimensions() {
-  window_->OnDocumentRootElementUnableToProvideOffsetDimensions();
+  window()->OnDocumentRootElementUnableToProvideOffsetDimensions();
 }
 
 void Document::DispatchOnLoadEvent() {
diff --git a/cobalt/dom/document.h b/cobalt/dom/document.h
index 89c65c0..0a7a4d9 100644
--- a/cobalt/dom/document.h
+++ b/cobalt/dom/document.h
@@ -103,11 +103,10 @@
                  public ApplicationLifecycleState::Observer {
  public:
   struct Options {
-    Options() : window(NULL), cookie_jar(NULL) {}
+    Options() : cookie_jar(NULL) {}
     explicit Options(const GURL& url_value)
-        : url(url_value), window(NULL), cookie_jar(NULL) {}
-    Options(const GURL& url_value, Window* window,
-            const base::Closure& hashchange_callback,
+        : url(url_value), cookie_jar(NULL) {}
+    Options(const GURL& url_value, const base::Closure& hashchange_callback,
             const scoped_refptr<base::BasicClock>& navigation_start_clock_value,
             const base::Callback<void(const GURL&)>& navigation_callback,
             const scoped_refptr<cssom::CSSStyleSheet> user_agent_style_sheet,
@@ -115,7 +114,6 @@
             network_bridge::CookieJar* cookie_jar,
             int dom_max_element_depth = 0)
         : url(url_value),
-          window(window),
           hashchange_callback(hashchange_callback),
           navigation_start_clock(navigation_start_clock_value),
           navigation_callback(navigation_callback),
@@ -125,7 +123,6 @@
           dom_max_element_depth(dom_max_element_depth) {}
 
     GURL url;
-    Window* window;
     base::Closure hashchange_callback;
     scoped_refptr<base::BasicClock> navigation_start_clock;
     base::Callback<void(const GURL&)> navigation_callback;
@@ -136,7 +133,8 @@
   };
 
   Document(HTMLElementContext* html_element_context,
-           const Options& options = Options());
+           const Options& options = Options(),
+           web::CspDelegate* csp_delegate = nullptr);
 
   // Web API: Node
   //
@@ -274,10 +272,11 @@
   // Returns whether the document has browsing context. Having the browsing
   // context means the document is shown on the screen.
   //   https://www.w3.org/TR/html50/browsers.html#browsing-context
-  bool HasBrowsingContext() const { return !!window_; }
+  bool HasBrowsingContext() const {
+    return window() && (window()->document().get() == this);
+  }
 
-  void set_window(Window* window) { window_ = window; }
-  const scoped_refptr<Window> window();
+  const scoped_refptr<Window> window() const;
 
   // Sets the active element of the document.
   void SetActiveElement(Element* active_element);
@@ -363,12 +362,7 @@
   }
 
   // Virtual for testing.
-  virtual web::CspDelegate* csp_delegate() const {
-    if (window_) {
-      return window_->csp_delegate();
-    }
-    return nullptr;
-  }
+  virtual web::CspDelegate* GetCSPDelegate() const;
 
   // Triggers a synchronous layout.
   scoped_refptr<render_tree::Node> DoSynchronousLayoutAndGetRenderTree();
@@ -414,7 +408,8 @@
   // Page Visibility fields.
   bool hidden() const { return visibility_state() == kVisibilityStateHidden; }
   VisibilityState visibility_state() const {
-    return application_lifecycle_state()->GetVisibilityState();
+    const ApplicationLifecycleState* state = application_lifecycle_state();
+    return state ? state->GetVisibilityState() : kVisibilityStateHidden;
   }
   const EventListenerScriptValue* onvisibilitychange() const {
     return GetAttributeEventListener(base::Tokens::visibilitychange());
@@ -548,8 +543,6 @@
   // situation more gracefully than crashing.
   base::WeakPtr<ApplicationLifecycleState> application_lifecycle_state_;
 
-  // Reference to the associated window object.
-  Window* window_;
   // Associated DOM implementation object.
   scoped_refptr<DOMImplementation> implementation_;
   // List of CSS style sheets.
diff --git a/cobalt/dom/document_test.cc b/cobalt/dom/document_test.cc
index b693173..4668a6b 100644
--- a/cobalt/dom/document_test.cc
+++ b/cobalt/dom/document_test.cc
@@ -37,6 +37,7 @@
 #include "cobalt/dom/testing/fake_document.h"
 #include "cobalt/dom/testing/html_collection_testing.h"
 #include "cobalt/dom/testing/stub_environment_settings.h"
+#include "cobalt/dom/testing/stub_window.h"
 #include "cobalt/dom/text.h"
 #include "cobalt/dom/ui_event.h"
 #include "cobalt/script/testing/mock_exception_state.h"
@@ -64,25 +65,31 @@
   DocumentTest();
   ~DocumentTest() override;
 
-  base::MessageLoop message_loop_;
-  testing::StubEnvironmentSettings environment_settings_;
+  const scoped_refptr<Document>& document() {
+    return window_->window()->document();
+  }
+
+  std::unique_ptr<testing::StubWindow> window_;
+  std::unique_ptr<HTMLElementContext> html_element_context_;
   std::unique_ptr<css_parser::Parser> css_parser_;
   std::unique_ptr<DomStatTracker> dom_stat_tracker_;
-  HTMLElementContext html_element_context_;
 };
 
 DocumentTest::DocumentTest()
-    : css_parser_(css_parser::Parser::Create()),
-      dom_stat_tracker_(new DomStatTracker("DocumentTest")),
-      html_element_context_(&environment_settings_, NULL, NULL,
-                            css_parser_.get(), NULL, NULL, NULL, NULL, NULL,
-                            NULL, NULL, NULL, NULL, NULL, NULL, NULL,
-                            dom_stat_tracker_.get(), "",
-                            base::kApplicationStateStarted, NULL, NULL) {
+    : window_(new testing::StubWindow),
+      css_parser_(css_parser::Parser::Create()),
+      dom_stat_tracker_(new DomStatTracker("DocumentTest")) {
   EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
+  window_->InitializeWindow();
+  html_element_context_.reset(new HTMLElementContext(
+      window_->web_context()->environment_settings(), NULL, NULL,
+      css_parser_.get(), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+      NULL, NULL, NULL, dom_stat_tracker_.get(), "",
+      base::kApplicationStateStarted, NULL, NULL));
 }
 
 DocumentTest::~DocumentTest() {
+  window_.reset();
   EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
 }
 
@@ -92,29 +99,43 @@
 
 TEST_F(DocumentTest, Create) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   ASSERT_TRUE(document);
 
   EXPECT_EQ(Node::kDocumentNode, document->node_type());
   EXPECT_EQ("#document", document->node_name());
 
   GURL url("http://a valid url");
-  document =
-      new testing::FakeDocument(&html_element_context_, Document::Options(url));
+  document = new testing::FakeDocument(html_element_context_.get(),
+                                       Document::Options(url));
   EXPECT_EQ(url.spec(), document->url());
   EXPECT_EQ(url.spec(), document->document_uri());
   EXPECT_EQ(url, document->location()->url());
 }
 
-TEST_F(DocumentTest, IsNotXMLDocument) {
+TEST_F(DocumentTest, MainDocumentIsNotXMLDocument) {
+  EXPECT_FALSE(document()->IsXMLDocument());
+}
+
+TEST_F(DocumentTest, NewDocumentIsNotXMLDocument) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   EXPECT_FALSE(document->IsXMLDocument());
 }
 
+TEST_F(DocumentTest, MainDocumentHasBrowsingContext) {
+  EXPECT_TRUE(document()->HasBrowsingContext());
+}
+
+TEST_F(DocumentTest, NewDocumentHasNoBrowsingContext) {
+  scoped_refptr<Document> document =
+      new testing::FakeDocument(html_element_context_.get());
+  EXPECT_FALSE(document->HasBrowsingContext());
+}
+
 TEST_F(DocumentTest, DocumentElement) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   EXPECT_EQ(NULL, document->document_element().get());
 
   scoped_refptr<Text> text = new Text(document, "test_text");
@@ -127,7 +148,7 @@
 
 TEST_F(DocumentTest, CreateElement) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   scoped_refptr<Element> element = document->CreateElement("element");
 
   EXPECT_EQ(Node::kElementNode, element->node_type());
@@ -144,7 +165,7 @@
 
 TEST_F(DocumentTest, CreateTextNode) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   scoped_refptr<Text> text = document->CreateTextNode("test_text");
 
   EXPECT_EQ(Node::kTextNode, text->node_type());
@@ -155,7 +176,7 @@
 
 TEST_F(DocumentTest, CreateComment) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   scoped_refptr<Comment> comment = document->CreateComment("test_comment");
 
   EXPECT_EQ(Node::kCommentNode, comment->node_type());
@@ -168,7 +189,7 @@
   StrictMock<MockExceptionState> exception_state;
   scoped_refptr<script::ScriptException> exception;
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   // Create an Event, the name is case insensitive.
   scoped_refptr<web::Event> event =
@@ -193,7 +214,7 @@
   StrictMock<MockExceptionState> exception_state;
   scoped_refptr<script::ScriptException> exception;
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   // Create an Event, the name is case insensitive.
   scoped_refptr<web::Event> event =
@@ -207,7 +228,7 @@
   StrictMock<MockExceptionState> exception_state;
   scoped_refptr<script::ScriptException> exception;
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   // Create an Event, the name is case insensitive.
   scoped_refptr<web::Event> event =
@@ -226,7 +247,7 @@
   StrictMock<MockExceptionState> exception_state;
   scoped_refptr<script::ScriptException> exception;
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   // Create an Event, the name is case insensitive.
   scoped_refptr<web::Event> event =
@@ -245,7 +266,7 @@
   StrictMock<MockExceptionState> exception_state;
   scoped_refptr<script::ScriptException> exception;
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   // Create an Event, the name is case insensitive.
   scoped_refptr<web::Event> event =
@@ -259,7 +280,7 @@
   StrictMock<MockExceptionState> exception_state;
   scoped_refptr<script::ScriptException> exception;
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   // Create an Event, the name is case insensitive.
   scoped_refptr<web::Event> event =
@@ -278,7 +299,7 @@
   StrictMock<MockExceptionState> exception_state;
   scoped_refptr<script::ScriptException> exception;
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   EXPECT_CALL(exception_state, SetException(_))
       .WillOnce(SaveArg<0>(&exception));
@@ -294,19 +315,19 @@
 
 TEST_F(DocumentTest, GetElementsByClassName) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   testing::TestGetElementsByClassName(document);
 }
 
 TEST_F(DocumentTest, GetElementsByTagName) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   testing::TestGetElementsByTagName(document);
 }
 
 TEST_F(DocumentTest, GetElementById) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   // Construct a tree:
   // document
@@ -340,34 +361,34 @@
 
 TEST_F(DocumentTest, Implementation) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   EXPECT_TRUE(document->implementation());
 }
 
 TEST_F(DocumentTest, Location) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   EXPECT_TRUE(document->location());
 }
 
 TEST_F(DocumentTest, StyleSheets) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   scoped_refptr<HTMLElement> element1 =
-      html_element_context_.html_element_factory()->CreateHTMLElement(
+      html_element_context_->html_element_factory()->CreateHTMLElement(
           document, base::Token(HTMLStyleElement::kTagName));
   element1->set_text_content(std::string("body { background-color: #D3D3D3 }"));
   document->AppendChild(element1);
 
   scoped_refptr<HTMLElement> element2 =
-      html_element_context_.html_element_factory()->CreateHTMLElement(
+      html_element_context_->html_element_factory()->CreateHTMLElement(
           document, base::Token(HTMLStyleElement::kTagName));
   element2->set_text_content(std::string("h1 { color: #00F }"));
   document->AppendChild(element2);
 
   scoped_refptr<HTMLElement> element3 =
-      html_element_context_.html_element_factory()->CreateHTMLElement(
+      html_element_context_->html_element_factory()->CreateHTMLElement(
           document, base::Token(HTMLStyleElement::kTagName));
   element3->set_text_content(std::string("p { color: #008000 }"));
   document->AppendChild(element3);
@@ -398,22 +419,22 @@
 
 TEST_F(DocumentTest, StyleSheetsAddedToFront) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
 
   scoped_refptr<HTMLElement> element1 =
-      html_element_context_.html_element_factory()->CreateHTMLElement(
+      html_element_context_->html_element_factory()->CreateHTMLElement(
           document, base::Token(HTMLStyleElement::kTagName));
   element1->set_text_content(std::string("body { background-color: #D3D3D3 }"));
   document->AppendChild(element1);
 
   scoped_refptr<HTMLElement> element2 =
-      html_element_context_.html_element_factory()->CreateHTMLElement(
+      html_element_context_->html_element_factory()->CreateHTMLElement(
           document, base::Token(HTMLStyleElement::kTagName));
   element2->set_text_content(std::string("h1 { color: #00F }"));
   document->InsertBefore(element2, element1);
 
   scoped_refptr<HTMLElement> element3 =
-      html_element_context_.html_element_factory()->CreateHTMLElement(
+      html_element_context_->html_element_factory()->CreateHTMLElement(
           document, base::Token(HTMLStyleElement::kTagName));
   element3->set_text_content(std::string("p { color: #008000 }"));
   document->InsertBefore(element3, element2);
@@ -444,7 +465,7 @@
 
 TEST_F(DocumentTest, HtmlElement) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   EXPECT_FALSE(document->html());
 
   scoped_refptr<Node> div =
@@ -459,7 +480,7 @@
 
 TEST_F(DocumentTest, HeadElement) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   EXPECT_FALSE(document->head());
 
   scoped_refptr<Node> html =
@@ -478,7 +499,7 @@
 
 TEST_F(DocumentTest, BodyElement) {
   scoped_refptr<Document> document =
-      new testing::FakeDocument(&html_element_context_);
+      new testing::FakeDocument(html_element_context_.get());
   EXPECT_FALSE(document->body());
 
   scoped_refptr<Node> html =
diff --git a/cobalt/dom/dom_stat_tracker.cc b/cobalt/dom/dom_stat_tracker.cc
index 093889f..2846654 100644
--- a/cobalt/dom/dom_stat_tracker.cc
+++ b/cobalt/dom/dom_stat_tracker.cc
@@ -68,8 +68,9 @@
 
   // Verify that all of the elements were removed from the document and
   // destroyed.
-  DCHECK_EQ(count_html_element_, 0);
-  DCHECK_EQ(count_html_element_document_, 0);
+  DCHECK_EQ(count_html_element_, 0) << count_html_element_ << " html elements.";
+  DCHECK_EQ(count_html_element_document_, 0)
+      << count_html_element_document_ << " html elements in the document.";
 
   event_video_start_delay_stop_watch_.Stop();
 }
diff --git a/cobalt/dom/event_queue.cc b/cobalt/dom/event_queue.cc
index ca851e8..1bd5282 100644
--- a/cobalt/dom/event_queue.cc
+++ b/cobalt/dom/event_queue.cc
@@ -44,23 +44,6 @@
   events_.push_back(event);
 }
 
-void EventQueue::EnqueueAndMaybeDispatchImmediately(
-    const scoped_refptr<web::Event>& event) {
-  DCHECK(message_loop_->BelongsToCurrentThread());
-
-  bool was_empty = events_.empty();
-
-  Enqueue(event);
-
-  if (was_empty) {
-    // We can only dispatch the event immediately if there aren't any existing
-    // events in the queue, because dom activities (including events) have to
-    // happen in order, and any existing events in the queue may mean that these
-    // events have to be dispatched after other activities not tracked here.
-    DispatchEvents();
-  }
-}
-
 void EventQueue::CancelAllEvents() {
   DCHECK(message_loop_->BelongsToCurrentThread());
 
diff --git a/cobalt/dom/event_queue.h b/cobalt/dom/event_queue.h
index 6aad863..a81e482 100644
--- a/cobalt/dom/event_queue.h
+++ b/cobalt/dom/event_queue.h
@@ -44,11 +44,6 @@
   explicit EventQueue(web::EventTarget* event_target);
 
   void Enqueue(const scoped_refptr<web::Event>& event);
-  // Try to dispatch the event immediately if there are no existing events in
-  // the queue, otherwise it has the same behavior as Enqueue(), which enqueues
-  // the event and the event will be dispatched after the existing events.
-  void EnqueueAndMaybeDispatchImmediately(
-      const scoped_refptr<web::Event>& event);
   void CancelAllEvents();
 
   void TraceMembers(script::Tracer* tracer) override;
diff --git a/cobalt/dom/event_queue_test.cc b/cobalt/dom/event_queue_test.cc
index be4a970..e8fc35f 100644
--- a/cobalt/dom/event_queue_test.cc
+++ b/cobalt/dom/event_queue_test.cc
@@ -90,42 +90,6 @@
   base::RunLoop().RunUntilIdle();
 }
 
-TEST_F(EventQueueTest, EnqueueAndMaybeDispatchImmediatelyTest) {
-  scoped_refptr<web::EventTarget> event_target =
-      new web::EventTarget(&environment_settings_);
-  scoped_refptr<web::Event> event = new web::Event(base::Token("event"));
-  std::unique_ptr<MockEventListener> event_listener =
-      MockEventListener::Create();
-  EventQueue event_queue(event_target.get());
-
-  event->set_target(event_target);
-  event_target->AddEventListener(
-      "event", FakeScriptValue<web::EventListener>(event_listener.get()),
-      false);
-
-  {
-    ::testing::InSequence s;
-    ExpectHandleEventCallWithEventAndTarget(event_listener.get(), event,
-                                            event_target);
-    // The event should be dispatched immediately as the queue is empty, so the
-    // expectation should be set before being enqueued.
-    event_queue.EnqueueAndMaybeDispatchImmediately(event);
-  }
-
-  {
-    ::testing::InSequence s;
-    event_queue.Enqueue(event);
-    // The event won't be dispatched immediately as the queue isn't empty, so
-    // the expectations can be set after being enqueued.
-    event_queue.EnqueueAndMaybeDispatchImmediately(event);
-    ExpectHandleEventCallWithEventAndTarget(event_listener.get(), event,
-                                            event_target);
-    ExpectHandleEventCallWithEventAndTarget(event_listener.get(), event,
-                                            event_target);
-    base::RunLoop().RunUntilIdle();
-  }
-}
-
 TEST_F(EventQueueTest, CancelAllEventsTest) {
   scoped_refptr<web::EventTarget> event_target =
       new web::EventTarget(&environment_settings_);
diff --git a/cobalt/dom/global_stats.cc b/cobalt/dom/global_stats.cc
index 66acba6..b589091 100644
--- a/cobalt/dom/global_stats.cc
+++ b/cobalt/dom/global_stats.cc
@@ -47,13 +47,13 @@
 GlobalStats::~GlobalStats() {}
 
 bool GlobalStats::CheckNoLeaks() {
-  DCHECK(num_attrs_ == 0);
-  DCHECK(num_dom_string_maps_ == 0);
-  DCHECK(num_dom_token_lists_ == 0);
-  DCHECK(num_html_collections_ == 0);
-  DCHECK(num_named_node_maps_ == 0);
-  DCHECK(num_nodes_ == 0);
-  DCHECK(num_node_lists_ == 0);
+  DCHECK(num_attrs_ == 0) << num_attrs_;
+  DCHECK(num_dom_string_maps_ == 0) << num_dom_string_maps_;
+  DCHECK(num_dom_token_lists_ == 0) << num_dom_token_lists_;
+  DCHECK(num_html_collections_ == 0) << num_html_collections_;
+  DCHECK(num_named_node_maps_ == 0) << num_named_node_maps_;
+  DCHECK(num_nodes_ == 0) << num_nodes_;
+  DCHECK(num_node_lists_ == 0) << num_node_lists_;
   return web::GlobalStats::GetInstance()->CheckNoLeaks() &&
          xhr::GlobalStats::GetInstance()->CheckNoLeaks() && num_attrs_ == 0 &&
          num_dom_string_maps_ == 0 && num_dom_token_lists_ == 0 &&
diff --git a/cobalt/dom/html_element.cc b/cobalt/dom/html_element.cc
index 970c56e..459be04 100644
--- a/cobalt/dom/html_element.cc
+++ b/cobalt/dom/html_element.cc
@@ -834,7 +834,7 @@
 
 void HTMLElement::SetStyleAttribute(const std::string& value) {
   Document* document = node_document();
-  web::CspDelegate* csp_delegate = document->csp_delegate();
+  web::CspDelegate* csp_delegate = document->GetCSPDelegate();
   if (value.empty() ||
       csp_delegate->AllowInline(
           web::CspDelegate::kStyle,
diff --git a/cobalt/dom/html_element_context.cc b/cobalt/dom/html_element_context.cc
index 7328cde..c3ca88a 100644
--- a/cobalt/dom/html_element_context.cc
+++ b/cobalt/dom/html_element_context.cc
@@ -55,7 +55,7 @@
 #endif  // !defined(COBALT_BUILD_TYPE_GOLD)
 
 HTMLElementContext::HTMLElementContext(
-    script::EnvironmentSettings* environment_settings,
+    web::EnvironmentSettings* environment_settings,
     loader::FetcherFactory* fetcher_factory,
     loader::LoaderFactory* loader_factory, cssom::CSSParser* css_parser,
     Parser* dom_parser, media::CanPlayTypeHandler* can_play_type_handler,
@@ -72,8 +72,7 @@
     loader::mesh::MeshCache* mesh_cache, DomStatTracker* dom_stat_tracker,
     const std::string& font_language_script,
     base::ApplicationState initial_application_state,
-    base::WaitableEvent* synchronous_loader_interrupt,
-    Performance* performance,
+    base::WaitableEvent* synchronous_loader_interrupt, Performance* performance,
     bool enable_inline_script_warnings, float video_playback_rate_multiplier)
     : environment_settings_(environment_settings),
       fetcher_factory_(fetcher_factory),
diff --git a/cobalt/dom/html_element_context.h b/cobalt/dom/html_element_context.h
index 4f4ff40..72e9de7 100644
--- a/cobalt/dom/html_element_context.h
+++ b/cobalt/dom/html_element_context.h
@@ -57,7 +57,7 @@
 #endif  // !defined(COBALT_BUILD_TYPE_GOLD)
 
   HTMLElementContext(
-      script::EnvironmentSettings* environment_settings,
+      web::EnvironmentSettings* environment_settings,
       loader::FetcherFactory* fetcher_factory,
       loader::LoaderFactory* loader_factory, cssom::CSSParser* css_parser,
       Parser* dom_parser, media::CanPlayTypeHandler* can_play_type_handler,
@@ -79,7 +79,7 @@
       float video_playback_rate_multiplier = 1.0);
   ~HTMLElementContext();
 
-  script::EnvironmentSettings* environment_settings() const {
+  web::EnvironmentSettings* environment_settings() const {
     return environment_settings_;
   }
 
@@ -166,10 +166,10 @@
  private:
 #if !defined(COBALT_BUILD_TYPE_GOLD)
   // StubEnvironmentSettings for no-args test constructor.
-  std::unique_ptr<script::EnvironmentSettings> stub_environment_settings_;
+  std::unique_ptr<web::EnvironmentSettings> stub_environment_settings_;
 #endif  // !defined(COBALT_BUILD_TYPE_GOLD)
 
-  script::EnvironmentSettings* environment_settings_;
+  web::EnvironmentSettings* environment_settings_;
   loader::FetcherFactory* const fetcher_factory_;
   loader::LoaderFactory* const loader_factory_;
   cssom::CSSParser* const css_parser_;
diff --git a/cobalt/dom/html_element_test.cc b/cobalt/dom/html_element_test.cc
index 9426a09..d30f4a7 100644
--- a/cobalt/dom/html_element_test.cc
+++ b/cobalt/dom/html_element_test.cc
@@ -21,12 +21,14 @@
 #include "cobalt/base/polymorphic_downcast.h"
 #include "cobalt/cssom/css_computed_style_data.h"
 #include "cobalt/cssom/css_declared_style_data.h"
+#include "cobalt/cssom/css_parser.h"
 #include "cobalt/cssom/keyword_value.h"
 #include "cobalt/cssom/testing/mock_css_parser.h"  // nogncheck
 #include "cobalt/cssom/viewport_size.h"
 #include "cobalt/dom/document.h"
 #include "cobalt/dom/dom_rect_list.h"
 #include "cobalt/dom/dom_stat_tracker.h"
+#include "cobalt/dom/global_stats.h"
 #include "cobalt/dom/html_body_element.h"
 #include "cobalt/dom/html_div_element.h"
 #include "cobalt/dom/html_element_context.h"
@@ -82,13 +84,33 @@
 class HTMLElementTest : public ::testing::Test {
  protected:
   HTMLElementTest()
-      : dom_stat_tracker_(new DomStatTracker("HTMLElementTest")),
-        html_element_context_(&environment_settings_, NULL, NULL, &css_parser_,
-                              NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
-                              NULL, NULL, NULL, NULL, dom_stat_tracker_.get(),
-                              "", base::kApplicationStateStarted, NULL, NULL),
-        document_(new testing::FakeDocument(&html_element_context_)) {}
-  ~HTMLElementTest() override {}
+      : window_(new testing::StubWindow),
+        dom_stat_tracker_(new DomStatTracker("HTMLElementTest")) {
+    EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
+    window_->set_css_parser(new cssom::testing::MockCSSParser);
+    // We expect one call to parse the user agent style sheet.
+    EXPECT_CALL(css_parser(), ParseStyleSheet(_, _))
+        .WillRepeatedly(Return(scoped_refptr<cssom::CSSStyleSheet>()));
+    window_->InitializeWindow();
+    html_element_context_.reset(new HTMLElementContext(
+        window_->web_context()->environment_settings(), NULL, NULL,
+        window_->css_parser(), NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+        NULL, NULL, NULL, NULL, dom_stat_tracker_.get(), "",
+        base::kApplicationStateStarted, NULL, NULL));
+  }
+  ~HTMLElementTest() override {
+    window_.reset();
+    EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
+  }
+
+  cssom::testing::MockCSSParser& css_parser() {
+    return *base::polymorphic_downcast<cssom::testing::MockCSSParser*>(
+        window_->css_parser());
+  }
+
+  const scoped_refptr<Document>& document() {
+    return window_->window()->document();
+  }
 
   // This creates simple DOM tree with mock layout boxes for all elements except
   // the last child element. It returns the root html element.
@@ -98,17 +120,15 @@
   void SetElementStyle(const scoped_refptr<cssom::CSSDeclaredStyleData>& data,
                        HTMLElement* html_element);
 
-  testing::StubEnvironmentSettings environment_settings_;
-  cssom::testing::MockCSSParser css_parser_;
+  std::unique_ptr<testing::StubWindow> window_;
   std::unique_ptr<DomStatTracker> dom_stat_tracker_;
-  HTMLElementContext html_element_context_;
-  scoped_refptr<Document> document_;
+  std::unique_ptr<HTMLElementContext> html_element_context_;
 };
 
 void HTMLElementTest::SetElementStyle(
     const scoped_refptr<cssom::CSSDeclaredStyleData>& data,
     HTMLElement* html_element) {
-  EXPECT_CALL(css_parser_,
+  EXPECT_CALL(css_parser(),
               ParseStyleDeclarationList(kFooBarDeclarationString, _))
       .WillOnce(Return(data));
   html_element->SetAttribute("style", kFooBarDeclarationString);
@@ -117,12 +137,13 @@
 scoped_refptr<HTMLElement>
 HTMLElementTest::CreateHTMLElementTreeWithMockLayoutBoxes(
     const char* null_terminated_element_names[]) {
-  DCHECK(!document_->IsXMLDocument());
+  DCHECK(!document()->IsXMLDocument());
   scoped_refptr<HTMLElement> root_html_element;
   scoped_refptr<HTMLElement> parent_html_element;
   do {
     scoped_refptr<HTMLElement> child_html_element(
-        document_->CreateElement(*null_terminated_element_names)
+        document()
+            ->CreateElement(*null_terminated_element_names)
             ->AsHTMLElement());
     DCHECK(child_html_element);
     child_html_element->css_computed_style_declaration()->SetData(
@@ -163,7 +184,7 @@
 TEST_F(HTMLElementTest, Dir) {
   for (size_t i = 0; i < arraysize(kHtmlElementTagNames); ++i) {
     scoped_refptr<HTMLElement> html_element =
-        document_->CreateElement(kHtmlElementTagNames[i])->AsHTMLElement();
+        document()->CreateElement(kHtmlElementTagNames[i])->AsHTMLElement();
     EXPECT_EQ("", html_element->dir());
 
     html_element->set_dir("invalid");
@@ -189,7 +210,7 @@
 TEST_F(HTMLElementTest, TabIndex) {
   for (size_t i = 0; i < arraysize(kHtmlElementTagNames); ++i) {
     scoped_refptr<HTMLElement> html_element =
-        document_->CreateElement(kHtmlElementTagNames[i])->AsHTMLElement();
+        document()->CreateElement(kHtmlElementTagNames[i])->AsHTMLElement();
 
     EXPECT_EQ(0, html_element->tab_index());
 
@@ -205,29 +226,26 @@
 }
 
 TEST_F(HTMLElementTest, Focus) {
-  // Give the document browsing context which is needed for focus to work.
-  testing::StubWindow window;
-  document_->set_window(window.window());
   // Give the document initial computed style.
-  document_->SetViewport(kViewSize);
+  document()->SetViewport(kViewSize);
 
   scoped_refptr<HTMLElement> html_element_1 =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<HTMLElement> html_element_2 =
-      document_->CreateElement("div")->AsHTMLElement();
-  document_->AppendChild(html_element_1);
-  document_->AppendChild(html_element_2);
-  EXPECT_FALSE(document_->active_element());
+      document()->CreateElement("div")->AsHTMLElement();
+  document()->AppendChild(html_element_1);
+  document()->AppendChild(html_element_2);
+  EXPECT_FALSE(document()->active_element());
 
   html_element_1->set_tab_index(-1);
   html_element_1->Focus();
-  ASSERT_TRUE(document_->active_element());
-  EXPECT_EQ(html_element_1, document_->active_element()->AsHTMLElement());
+  ASSERT_TRUE(document()->active_element());
+  EXPECT_EQ(html_element_1, document()->active_element()->AsHTMLElement());
 
   html_element_2->set_tab_index(-1);
   html_element_2->Focus();
-  ASSERT_TRUE(document_->active_element());
-  EXPECT_EQ(html_element_2, document_->active_element()->AsHTMLElement());
+  ASSERT_TRUE(document()->active_element());
+  EXPECT_EQ(html_element_2, document()->active_element()->AsHTMLElement());
 
   // Make sure that if we try to focus an element that has display set to none,
   // it will not take the focus.
@@ -237,70 +255,64 @@
       cssom::kDisplayProperty, cssom::KeywordValue::GetNone(), false);
   SetElementStyle(display_none_style, html_element_1);
   html_element_1->Focus();
-  ASSERT_TRUE(document_->active_element());
-  EXPECT_EQ(html_element_2, document_->active_element()->AsHTMLElement());
+  ASSERT_TRUE(document()->active_element());
+  EXPECT_EQ(html_element_2, document()->active_element()->AsHTMLElement());
 
   // Make sure that if we try to focus an element whose ancestor has display
   // set to none, it will not take the focus.
   scoped_refptr<HTMLElement> html_element_3 =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<HTMLElement> html_element_4 =
-      document_->CreateElement("div")->AsHTMLElement();
-  document_->AppendChild(html_element_3);
+      document()->CreateElement("div")->AsHTMLElement();
+  document()->AppendChild(html_element_3);
   html_element_3->AppendChild(html_element_4);
 
   html_element_4->set_tab_index(-1);
   SetElementStyle(display_none_style, html_element_3);
   html_element_4->Focus();
-  ASSERT_TRUE(document_->active_element());
-  EXPECT_EQ(html_element_2, document_->active_element()->AsHTMLElement());
+  ASSERT_TRUE(document()->active_element());
+  EXPECT_EQ(html_element_2, document()->active_element()->AsHTMLElement());
 }
 
 TEST_F(HTMLElementTest, Blur) {
-  // Give the document browsing context which is needed for focus to work.
-  testing::StubWindow window;
-  document_->set_window(window.window());
   // Give the document initial computed style.
-  document_->SetViewport(kViewSize);
+  document()->SetViewport(kViewSize);
 
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
-  document_->AppendChild(html_element);
-  EXPECT_FALSE(document_->active_element());
+      document()->CreateElement("div")->AsHTMLElement();
+  document()->AppendChild(html_element);
+  EXPECT_FALSE(document()->active_element());
 
   html_element->set_tab_index(-1);
   html_element->Focus();
-  ASSERT_TRUE(document_->active_element());
-  EXPECT_EQ(html_element, document_->active_element()->AsHTMLElement());
+  ASSERT_TRUE(document()->active_element());
+  EXPECT_EQ(html_element, document()->active_element()->AsHTMLElement());
 
   html_element->Blur();
-  EXPECT_FALSE(document_->active_element());
+  EXPECT_FALSE(document()->active_element());
 }
 
 TEST_F(HTMLElementTest, RemoveActiveElementShouldRunBlur) {
-  // Give the document browsing context which is needed for focus to work.
-  testing::StubWindow window;
-  document_->set_window(window.window());
   // Give the document initial computed style.
-  document_->SetViewport(kViewSize);
+  document()->SetViewport(kViewSize);
 
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
-  document_->AppendChild(html_element);
-  EXPECT_FALSE(document_->active_element());
+      document()->CreateElement("div")->AsHTMLElement();
+  document()->AppendChild(html_element);
+  EXPECT_FALSE(document()->active_element());
 
   html_element->set_tab_index(-1);
   html_element->Focus();
-  ASSERT_TRUE(document_->active_element());
-  EXPECT_EQ(html_element, document_->active_element()->AsHTMLElement());
+  ASSERT_TRUE(document()->active_element());
+  EXPECT_EQ(html_element, document()->active_element()->AsHTMLElement());
 
-  document_->RemoveChild(html_element);
-  EXPECT_FALSE(document_->active_element());
+  document()->RemoveChild(html_element);
+  EXPECT_FALSE(document()->active_element());
 }
 
 TEST_F(HTMLElementTest, LayoutBoxesGetter) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
 
   std::unique_ptr<MockLayoutBoxes> mock_layout_boxes(new MockLayoutBoxes);
   MockLayoutBoxes* saved_mock_layout_boxes_ptr = mock_layout_boxes.get();
@@ -319,7 +331,7 @@
 
 TEST_F(HTMLElementTest, GetBoundingClientRectWithoutLayoutBox) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<DOMRect> rect = html_element->GetBoundingClientRect();
   DCHECK(rect);
   EXPECT_FLOAT_EQ(rect->x(), 0.0f);
@@ -336,7 +348,7 @@
 //   https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-element-clienttop
 TEST_F(HTMLElementTest, ClientTop) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
 
   // 1. If the element has no associated CSS layout box, return zero.
   EXPECT_FLOAT_EQ(html_element->client_top(), 0.0f);
@@ -371,7 +383,7 @@
 //   https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-element-clientleft
 TEST_F(HTMLElementTest, ClientLeft) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
 
   // 1. If the element has no associated CSS layout box, return zero.
   EXPECT_FLOAT_EQ(html_element->client_left(), 0.0f);
@@ -655,7 +667,7 @@
 //   https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-htmlelement-offsetwidth
 TEST_F(HTMLElementTest, OffsetWidth) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
 
   // 1. If the element does not have any associated CSS layout box return zero
   // and terminate this algorithm.
@@ -679,7 +691,7 @@
 //   https://www.w3.org/TR/2013/WD-cssom-view-20131217/#dom-htmlelement-offsetheight
 TEST_F(HTMLElementTest, OffsetHeight) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
 
   // 1. If the element does not have any associated CSS layout box return zero
   // and terminate this algorithm.
@@ -701,7 +713,7 @@
 
 TEST_F(HTMLElementTest, SetAttributeMatchesGetAttribute) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   html_element->SetAttribute("foo", "bar");
   EXPECT_EQ(1, html_element->attributes()->length());
   EXPECT_EQ("bar", html_element->GetAttribute("foo").value());
@@ -709,10 +721,10 @@
 
 TEST_F(HTMLElementTest, SetAttributeStyleSetsElementStyle) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<cssom::CSSDeclaredStyleData> style(
       new cssom::CSSDeclaredStyleData());
-  EXPECT_CALL(css_parser_, ParseStyleDeclarationList("", _))
+  EXPECT_CALL(css_parser(), ParseStyleDeclarationList("", _))
       .WillOnce(Return(style));
   html_element->SetAttribute("style", "");
   EXPECT_EQ(style, html_element->style()->data());
@@ -720,10 +732,10 @@
 
 TEST_F(HTMLElementTest, SetAttributeStyleReplacesExistingElementStyle) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<cssom::CSSDeclaredStyleData> style(
       new cssom::CSSDeclaredStyleData());
-  EXPECT_CALL(css_parser_,
+  EXPECT_CALL(css_parser(),
               ParseStyleDeclarationList(kDisplayInlineDeclarationString, _))
       .WillOnce(Return(style));
   html_element->SetAttribute("style", kDisplayInlineDeclarationString);
@@ -735,7 +747,7 @@
 
   scoped_refptr<cssom::CSSDeclaredStyleData> new_style(
       new cssom::CSSDeclaredStyleData());
-  EXPECT_CALL(css_parser_,
+  EXPECT_CALL(css_parser(),
               ParseStyleDeclarationList(kFooBarDeclarationString, _))
       .WillOnce(Return(new_style));
   html_element->SetAttribute("style", kFooBarDeclarationString);
@@ -747,10 +759,10 @@
 
 TEST_F(HTMLElementTest, GetAttributeStyleMatchesSetAttributeStyle) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<cssom::CSSDeclaredStyleData> style(
       new cssom::CSSDeclaredStyleData());
-  EXPECT_CALL(css_parser_,
+  EXPECT_CALL(css_parser(),
               ParseStyleDeclarationList(kFooBarDeclarationString, _))
       .WillOnce(Return(style));
   html_element->SetAttribute("style", kFooBarDeclarationString);
@@ -762,16 +774,16 @@
 TEST_F(HTMLElementTest,
        GetAttributeStyleDoesNotMatchSetAttributeStyleAfterStyleMutation) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<cssom::CSSDeclaredStyleData> style(
       new cssom::CSSDeclaredStyleData());
-  EXPECT_CALL(css_parser_,
+  EXPECT_CALL(css_parser(),
               ParseStyleDeclarationList(kFooBarDeclarationString, _))
       .WillOnce(Return(style));
   html_element->SetAttribute("style", kFooBarDeclarationString);
   EXPECT_EQ(1, html_element->attributes()->length());
-  EXPECT_CALL(css_parser_, ParsePropertyIntoDeclarationData("display", "inline",
-                                                            _, style.get()))
+  EXPECT_CALL(css_parser(), ParsePropertyIntoDeclarationData(
+                                "display", "inline", _, style.get()))
       .WillOnce(InvokeCallback0(base::Bind(
           &cssom::CSSDeclaredStyleData::SetPropertyValueAndImportance,
           base::Unretained(style.get()), cssom::kDisplayProperty,
@@ -785,16 +797,16 @@
 TEST_F(HTMLElementTest,
        GetAttributeStyleMatchesSerializedStyleAfterStyleMutation) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   scoped_refptr<cssom::CSSDeclaredStyleData> style(
       new cssom::CSSDeclaredStyleData());
-  EXPECT_CALL(css_parser_,
+  EXPECT_CALL(css_parser(),
               ParseStyleDeclarationList(kFooBarDeclarationString, _))
       .WillOnce(Return(style));
   html_element->SetAttribute("style", kFooBarDeclarationString);
   EXPECT_EQ(1, html_element->attributes()->length());
-  EXPECT_CALL(css_parser_, ParsePropertyIntoDeclarationData("display", "inline",
-                                                            _, style.get()))
+  EXPECT_CALL(css_parser(), ParsePropertyIntoDeclarationData(
+                                "display", "inline", _, style.get()))
       .WillOnce(InvokeCallback0(base::Bind(
           &cssom::CSSDeclaredStyleData::SetPropertyValueAndImportance,
           base::Unretained(style.get()), cssom::kDisplayProperty,
@@ -807,7 +819,7 @@
 
 TEST_F(HTMLElementTest, Duplicate) {
   scoped_refptr<HTMLElement> html_element =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   html_element->SetAttribute("a", "1");
   html_element->SetAttribute("b", "2");
   scoped_refptr<HTMLElement> new_html_element =
diff --git a/cobalt/dom/html_link_element.cc b/cobalt/dom/html_link_element.cc
index 17a6de1..d01d0e0 100644
--- a/cobalt/dom/html_link_element.cc
+++ b/cobalt/dom/html_link_element.cc
@@ -192,7 +192,7 @@
   // attribute, the origin being the origin of the link element's Document, and
   // the default origin behaviour set to taint.
   csp::SecurityCallback csp_callback = base::Bind(
-      &web::CspDelegate::CanLoad, base::Unretained(document->csp_delegate()),
+      &web::CspDelegate::CanLoad, base::Unretained(document->GetCSPDelegate()),
       GetCspResourceTypeForRel(rel()));
 
   fetched_last_url_origin_ = loader::Origin();
diff --git a/cobalt/dom/html_media_element.cc b/cobalt/dom/html_media_element.cc
index d496b01..e4fb384b 100644
--- a/cobalt/dom/html_media_element.cc
+++ b/cobalt/dom/html_media_element.cc
@@ -835,8 +835,8 @@
   DCHECK(!media_source_);
   if (url.SchemeIs(kMediaSourceUrlProtocol)) {
     // Check whether url is allowed by security policy.
-    if (!node_document()->csp_delegate()->CanLoad(web::CspDelegate::kMedia, url,
-                                                  false)) {
+    if (!node_document()->GetCSPDelegate()->CanLoad(web::CspDelegate::kMedia,
+                                                    url, false)) {
       DLOG(INFO) << "URL " << url << " is rejected by security policy.";
       NoneSupported("URL is rejected by security policy.");
       return;
@@ -883,7 +883,7 @@
   } else {
     csp::SecurityCallback csp_callback =
         base::Bind(&web::CspDelegate::CanLoad,
-                   base::Unretained(node_document()->csp_delegate()),
+                   base::Unretained(node_document()->GetCSPDelegate()),
                    web::CspDelegate::kMedia);
     request_mode_ = GetRequestMode(GetAttribute("crossOrigin"));
     DCHECK(node_document()->location());
diff --git a/cobalt/dom/html_meta_element.cc b/cobalt/dom/html_meta_element.cc
index 3852a5a..5f58d40 100644
--- a/cobalt/dom/html_meta_element.cc
+++ b/cobalt/dom/html_meta_element.cc
@@ -42,7 +42,7 @@
                                           ? csp::kHeaderSourceMeta
                                           : csp::kHeaderSourceMetaOutsideHead;
 
-    node_document()->csp_delegate()->OnReceiveHeader(
+    node_document()->GetCSPDelegate()->OnReceiveHeader(
         csp_text, csp::kHeaderTypeEnforce, header_source);
   }
 
diff --git a/cobalt/dom/html_script_element.cc b/cobalt/dom/html_script_element.cc
index 42f7997..1dda5a4 100644
--- a/cobalt/dom/html_script_element.cc
+++ b/cobalt/dom/html_script_element.cc
@@ -79,15 +79,12 @@
 }
 
 std::string HTMLScriptElement::src() const {
-  auto src = GetAttribute("src");
-  if (!src.has_value()) {
-    return "";
+  std::string src = GetAttribute("src").value_or("");
+  if (src == "" || !node_document()) {
+    return src;
   }
-  if (!node_document()) {
-    return src.value();
-  }
-  const GURL& base_url = node_document()->location()->url();
-  return base_url.Resolve(src.value()).spec();
+  GURL url = node_document()->location()->url().Resolve(src);
+  return url.is_valid() ? url.spec() : src;
 }
 
 base::Optional<std::string> HTMLScriptElement::cross_origin() const {
@@ -304,7 +301,7 @@
 
   // https://www.w3.org/TR/CSP2/#directive-script-src
 
-  web::CspDelegate* csp_delegate = document_->csp_delegate();
+  web::CspDelegate* csp_delegate = document_->GetCSPDelegate();
   // If the script element has a valid nonce, we always permit it, regardless
   // of its URL or inline nature.
   const bool bypass_csp =
@@ -347,10 +344,11 @@
               csp_callback, request_mode_,
               document_->location() ? document_->location()->GetOriginAsObject()
                                     : loader::Origin(),
-              disk_cache::kUncompiledScript),
+              disk_cache::kUncompiledScript, net::HttpRequestHeaders()),
           base::Bind(&loader::TextDecoder::Create,
                      base::Bind(&HTMLScriptElement::OnSyncContentProduced,
                                 base::Unretained(this)),
+                     loader::TextDecoder::ResponseStartedCallback(),
                      loader::Decoder::OnCompleteFunction()),
           base::Bind(&HTMLScriptElement::OnSyncLoadingComplete,
                      base::Unretained(this)));
diff --git a/cobalt/dom/html_style_element.cc b/cobalt/dom/html_style_element.cc
index 1706b3f..d5d0be0 100644
--- a/cobalt/dom/html_style_element.cc
+++ b/cobalt/dom/html_style_element.cc
@@ -72,7 +72,7 @@
     return;
   }
 
-  web::CspDelegate* csp_delegate = document->csp_delegate();
+  web::CspDelegate* csp_delegate = document->GetCSPDelegate();
   // If the style element has a valid nonce, we always permit it.
   const bool bypass_csp = csp_delegate->IsValidNonce(
       web::CspDelegate::kStyle, GetAttribute("nonce").value_or(""));
diff --git a/cobalt/dom/media_source.cc b/cobalt/dom/media_source.cc
index 1dcc7d3..a3571f1 100644
--- a/cobalt/dom/media_source.cc
+++ b/cobalt/dom/media_source.cc
@@ -110,13 +110,13 @@
 // NOTE: This only works when IsAsynchronousReductionEnabled() returns true,
 //       and it is currently only enabled for buffer append.
 // The default value is 16 KB.  Set to 0 will disable immediate job completely.
-int GetMinSizeForImmediateJob(script::EnvironmentSettings* settings) {
-  const int kDefaultMinSize = 16 * 1024;
-  auto min_size =
-      GetMediaSettings(settings)->GetMinSizeForImmediateJob().value_or(
-          kDefaultMinSize);
-  DCHECK_GE(min_size, 0);
-  return min_size;
+int GetMaxSizeForImmediateJob(script::EnvironmentSettings* settings) {
+  const int kDefaultMaxSize = 16 * 1024;
+  auto max_size =
+      GetMediaSettings(settings)->GetMaxSizeForImmediateJob().value_or(
+          kDefaultMaxSize);
+  DCHECK_GE(max_size, 0);
+  return max_size;
 }
 
 }  // namespace
@@ -125,7 +125,7 @@
     : web::EventTarget(settings),
       algorithm_offload_enabled_(IsAlgorithmOffloadEnabled(settings)),
       asynchronous_reduction_enabled_(IsAsynchronousReductionEnabled(settings)),
-      min_size_for_immediate_job_(GetMinSizeForImmediateJob(settings)),
+      max_size_for_immediate_job_(GetMaxSizeForImmediateJob(settings)),
       default_algorithm_runner_(asynchronous_reduction_enabled_),
       chunk_demuxer_(NULL),
       ready_state_(kMediaSourceReadyStateClosed),
@@ -137,8 +137,8 @@
             << (algorithm_offload_enabled_ ? "enabled" : "disabled");
   LOG(INFO) << "Asynchronous reduction is "
             << (asynchronous_reduction_enabled_ ? "enabled" : "disabled");
-  LOG(INFO) << "Min size of immediate job is set to "
-            << min_size_for_immediate_job_;
+  LOG(INFO) << "Max size of immediate job is set to "
+            << max_size_for_immediate_job_;
 }
 
 MediaSource::~MediaSource() { SetReadyState(kMediaSourceReadyStateClosed); }
@@ -243,9 +243,8 @@
   ChunkDemuxer::Status status = chunk_demuxer_->AddId(guid, type);
   switch (status) {
     case ChunkDemuxer::kOk:
-      source_buffer = new SourceBuffer(settings, guid, this,
-                                       asynchronous_reduction_enabled_,
-                                       chunk_demuxer_, &event_queue_);
+      source_buffer =
+          new SourceBuffer(settings, guid, this, chunk_demuxer_, &event_queue_);
       break;
     case ChunkDemuxer::kNotSupported:
       web::DOMException::Raise(web::DOMException::kNotSupportedErr,
@@ -582,7 +581,7 @@
     return &default_algorithm_runner_;
   }
   if (asynchronous_reduction_enabled_ &&
-      job_size <= min_size_for_immediate_job_) {
+      job_size <= max_size_for_immediate_job_) {
     // Append without posting new tasks is only supported on the default runner.
     return &default_algorithm_runner_;
   }
diff --git a/cobalt/dom/media_source.h b/cobalt/dom/media_source.h
index 65ab2a8..1c84e8b 100644
--- a/cobalt/dom/media_source.h
+++ b/cobalt/dom/media_source.h
@@ -144,10 +144,10 @@
   // Set to true to reduce asynchronous behaviors.  For example, queued events
   // will be dispatached immediately when possible.
   const bool asynchronous_reduction_enabled_;
-  // Only used when |asynchronous_reduction_enabled_| is set true, where any
+  // Only used when |asynchronous_reduction_enabled_| is set to true, where any
   // buffer append job smaller than its value will happen immediately instead of
   // being scheduled asynchronously.
-  const int min_size_for_immediate_job_;
+  const int max_size_for_immediate_job_;
 
   // The default algorithm runner runs all steps on the web thread.
   DefaultAlgorithmRunner<SourceBufferAlgorithm> default_algorithm_runner_;
diff --git a/cobalt/dom/media_source_settings.cc b/cobalt/dom/media_source_settings.cc
index a330f83..d39602a 100644
--- a/cobalt/dom/media_source_settings.cc
+++ b/cobalt/dom/media_source_settings.cc
@@ -46,9 +46,15 @@
       LOG(INFO) << name << ": set to " << value;
       return true;
     }
-  } else if (name == "MediaSource.MinSizeForImmediateJob") {
+  } else if (name == "MediaSource.MaxSizeForImmediateJob") {
     if (value >= 0) {
-      min_size_for_immediate_job_ = value;
+      max_size_for_immediate_job_ = value;
+      LOG(INFO) << name << ": set to " << value;
+      return true;
+    }
+  } else if (name == "MediaSource.MaxSourceBufferAppendSizeInBytes") {
+    if (value > 0) {
+      max_source_buffer_append_size_in_bytes_ = value;
       LOG(INFO) << name << ": set to " << value;
       return true;
     }
diff --git a/cobalt/dom/media_source_settings.h b/cobalt/dom/media_source_settings.h
index 8d1b0be..a122834 100644
--- a/cobalt/dom/media_source_settings.h
+++ b/cobalt/dom/media_source_settings.h
@@ -35,7 +35,8 @@
   virtual base::Optional<int> GetMinimumProcessorCountToOffloadAlgorithm()
       const = 0;
   virtual base::Optional<bool> IsAsynchronousReductionEnabled() const = 0;
-  virtual base::Optional<int> GetMinSizeForImmediateJob() const = 0;
+  virtual base::Optional<int> GetMaxSizeForImmediateJob() const = 0;
+  virtual base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const = 0;
 
  protected:
   MediaSourceSettings() = default;
@@ -63,9 +64,13 @@
     base::AutoLock auto_lock(lock_);
     return is_asynchronous_reduction_enabled_;
   }
-  base::Optional<int> GetMinSizeForImmediateJob() const override {
+  base::Optional<int> GetMaxSizeForImmediateJob() const override {
     base::AutoLock auto_lock(lock_);
-    return min_size_for_immediate_job_;
+    return max_size_for_immediate_job_;
+  }
+  base::Optional<int> GetMaxSourceBufferAppendSizeInBytes() const override {
+    base::AutoLock auto_lock(lock_);
+    return max_source_buffer_append_size_in_bytes_;
   }
 
   // Returns true when the setting associated with `name` is set to `value`.
@@ -78,7 +83,8 @@
   base::Optional<int> source_buffer_evict_extra_in_bytes_;
   base::Optional<int> minimum_processor_count_to_offload_algorithm_;
   base::Optional<bool> is_asynchronous_reduction_enabled_;
-  base::Optional<int> min_size_for_immediate_job_;
+  base::Optional<int> max_size_for_immediate_job_;
+  base::Optional<int> max_source_buffer_append_size_in_bytes_;
 };
 
 }  // namespace dom
diff --git a/cobalt/dom/media_source_settings_test.cc b/cobalt/dom/media_source_settings_test.cc
index 27d9e9e..d8a27ed 100644
--- a/cobalt/dom/media_source_settings_test.cc
+++ b/cobalt/dom/media_source_settings_test.cc
@@ -26,7 +26,8 @@
   EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
   EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
   EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
-  EXPECT_FALSE(impl.GetMinSizeForImmediateJob());
+  EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
+  EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
 }
 
 TEST(MediaSourceSettingsImplTest, SunnyDay) {
@@ -36,12 +37,14 @@
   ASSERT_TRUE(
       impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 101));
   ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
-  ASSERT_TRUE(impl.Set("MediaSource.MinSizeForImmediateJob", 103));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 103));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 100000));
 
   EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 100);
   EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 101);
   EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
-  EXPECT_EQ(impl.GetMinSizeForImmediateJob().value(), 103);
+  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 103);
+  EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 100000);
 }
 
 TEST(MediaSourceSettingsImplTest, RainyDay) {
@@ -51,12 +54,14 @@
   ASSERT_FALSE(
       impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", -101));
   ASSERT_FALSE(impl.Set("MediaSource.EnableAsynchronousReduction", 2));
-  ASSERT_FALSE(impl.Set("MediaSource.MinSizeForImmediateJob", -103));
+  ASSERT_FALSE(impl.Set("MediaSource.MaxSizeForImmediateJob", -103));
+  ASSERT_FALSE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 0));
 
   EXPECT_FALSE(impl.GetSourceBufferEvictExtraInBytes());
   EXPECT_FALSE(impl.GetMinimumProcessorCountToOffloadAlgorithm());
   EXPECT_FALSE(impl.IsAsynchronousReductionEnabled());
-  EXPECT_FALSE(impl.GetMinSizeForImmediateJob());
+  EXPECT_FALSE(impl.GetMaxSizeForImmediateJob());
+  EXPECT_FALSE(impl.GetMaxSourceBufferAppendSizeInBytes());
 }
 
 TEST(MediaSourceSettingsImplTest, ZeroValuesWork) {
@@ -66,12 +71,13 @@
   ASSERT_TRUE(
       impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
   ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
-  ASSERT_TRUE(impl.Set("MediaSource.MinSizeForImmediateJob", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
+  // O is an invalid value for "MediaSource.MaxSourceBufferAppendSizeInBytes".
 
   EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 0);
   EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 0);
   EXPECT_FALSE(impl.IsAsynchronousReductionEnabled().value());
-  EXPECT_EQ(impl.GetMinSizeForImmediateJob().value(), 0);
+  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 0);
 }
 
 TEST(MediaSourceSettingsImplTest, Updatable) {
@@ -81,18 +87,21 @@
   ASSERT_TRUE(
       impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 0));
   ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 0));
-  ASSERT_TRUE(impl.Set("MediaSource.MinSizeForImmediateJob", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 0));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 1));
 
   ASSERT_TRUE(impl.Set("MediaSource.SourceBufferEvictExtraInBytes", 1));
   ASSERT_TRUE(
       impl.Set("MediaSource.MinimumProcessorCountToOffloadAlgorithm", 1));
   ASSERT_TRUE(impl.Set("MediaSource.EnableAsynchronousReduction", 1));
-  ASSERT_TRUE(impl.Set("MediaSource.MinSizeForImmediateJob", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSizeForImmediateJob", 1));
+  ASSERT_TRUE(impl.Set("MediaSource.MaxSourceBufferAppendSizeInBytes", 2));
 
   EXPECT_EQ(impl.GetSourceBufferEvictExtraInBytes().value(), 1);
   EXPECT_EQ(impl.GetMinimumProcessorCountToOffloadAlgorithm().value(), 1);
   EXPECT_TRUE(impl.IsAsynchronousReductionEnabled().value());
-  EXPECT_EQ(impl.GetMinSizeForImmediateJob().value(), 1);
+  EXPECT_EQ(impl.GetMaxSizeForImmediateJob().value(), 1);
+  EXPECT_EQ(impl.GetMaxSourceBufferAppendSizeInBytes().value(), 2);
 }
 
 TEST(MediaSourceSettingsImplTest, InvalidSettingNames) {
diff --git a/cobalt/dom/node.cc b/cobalt/dom/node.cc
index bdd6209..26c78eb 100644
--- a/cobalt/dom/node.cc
+++ b/cobalt/dom/node.cc
@@ -440,9 +440,7 @@
 Text* Node::AsText() { return NULL; }
 
 const base::DebuggerHooks& Node::debugger_hooks() const {
-  return base::polymorphic_downcast<DOMSettings*>(
-             node_document()->html_element_context()->environment_settings())
-      ->debugger_hooks();
+  return environment_settings()->debugger_hooks();
 }
 
 void Node::TraceMembers(script::Tracer* tracer) {
diff --git a/cobalt/dom/on_screen_keyboard_test.cc b/cobalt/dom/on_screen_keyboard_test.cc
index 8a8573c..b567585 100644
--- a/cobalt/dom/on_screen_keyboard_test.cc
+++ b/cobalt/dom/on_screen_keyboard_test.cc
@@ -386,7 +386,7 @@
     let promise;
     let logString;
   )";
-  EXPECT_TRUE(EvaluateScript(let_script, NULL));
+  EXPECT_TRUE(EvaluateScript(let_script));
   const char event_script[] = R"(
     logString = '(empty)';
     window.onScreenKeyboard.onshow = function() {
@@ -436,7 +436,7 @@
     let promise;
     let logString;
   )";
-  EXPECT_TRUE(EvaluateScript(let_script, NULL));
+  EXPECT_TRUE(EvaluateScript(let_script));
   const char event_script[] = R"(
     logString = '(empty)';
     window.onScreenKeyboard.onhide = function() {
@@ -485,7 +485,7 @@
     let promise;
     let logString;
   )";
-  EXPECT_TRUE(EvaluateScript(let_script, NULL));
+  EXPECT_TRUE(EvaluateScript(let_script));
   const char event_script[] = R"(
     logString = '(empty)';
     window.onScreenKeyboard.onfocus = function() {
@@ -534,7 +534,7 @@
     let promise;
     let logString;
   )";
-  EXPECT_TRUE(EvaluateScript(let_script, NULL));
+  EXPECT_TRUE(EvaluateScript(let_script));
   const char event_script[] = R"(
     logString = '(empty)';
     window.onScreenKeyboard.onblur = function() {
@@ -605,7 +605,7 @@
     window.onScreenKeyboard.keepFocus = false;
     window.onScreenKeyboard.keepFocus = true;
   )";
-  EXPECT_TRUE(EvaluateScript(script, NULL));
+  EXPECT_TRUE(EvaluateScript(script));
 }
 
 TEST_F(OnScreenKeyboardTest, SetBackgroundColor) {
diff --git a/cobalt/dom/rule_matching_test.cc b/cobalt/dom/rule_matching_test.cc
index e11362e..b7f4006 100644
--- a/cobalt/dom/rule_matching_test.cc
+++ b/cobalt/dom/rule_matching_test.cc
@@ -26,6 +26,7 @@
 #include "cobalt/dom/document.h"
 #include "cobalt/dom/dom_stat_tracker.h"
 #include "cobalt/dom/element.h"
+#include "cobalt/dom/global_stats.h"
 #include "cobalt/dom/html_collection.h"
 #include "cobalt/dom/html_element.h"
 #include "cobalt/dom/html_element_context.h"
@@ -38,6 +39,8 @@
 #include "cobalt/dom_parser/parser.h"
 #include "cobalt/script/script_exception.h"
 #include "cobalt/script/testing/mock_exception_state.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/testing/stub_web_context.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 using cobalt::cssom::ViewportSize;
@@ -51,47 +54,74 @@
 class RuleMatchingTest : public ::testing::Test {
  protected:
   RuleMatchingTest()
-      : css_parser_(css_parser::Parser::Create()),
+      : window_(new testing::StubWindow),
         dom_parser_(new dom_parser::Parser()),
-        dom_stat_tracker_(new DomStatTracker("RuleMatchingTest")),
-        html_element_context_(&environment_settings_, NULL, NULL,
-                              css_parser_.get(), dom_parser_.get(), NULL, NULL,
-                              NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
-                              NULL, dom_stat_tracker_.get(), "",
-                              base::kApplicationStateStarted, NULL, NULL),
-        document_(new testing::FakeDocument(&html_element_context_)),
-        root_(document_->CreateElement("html")->AsHTMLElement()),
-        head_(document_->CreateElement("head")->AsHTMLElement()),
-        body_(document_->CreateElement("body")->AsHTMLElement()) {
+        dom_stat_tracker_(new DomStatTracker("RuleMatchingTest")) {
+    EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
+    window_->InitializeWindow();
+    html_element_context_.reset(new HTMLElementContext(
+        window_->web_context()->environment_settings(), NULL, NULL,
+        css_parser(), dom_parser_.get(), NULL, NULL, NULL, NULL, NULL, NULL,
+        NULL, NULL, NULL, NULL, NULL, dom_stat_tracker_.get(), "",
+        base::kApplicationStateStarted, NULL, NULL));
+    root_ = document()->CreateElement("html")->AsHTMLElement();
+    head_ = document()->CreateElement("head")->AsHTMLElement();
+    body_ = document()->CreateElement("body")->AsHTMLElement();
     root_->AppendChild(head_);
     root_->AppendChild(body_);
-    document_->AppendChild(root_);
+    document()->AppendChild(root_);
   }
 
-  ~RuleMatchingTest() override {}
+  cssom::CSSParser* css_parser() { return window_->css_parser(); }
+
+  const scoped_refptr<Document>& document() {
+    return window_->window()->document();
+  }
+
+  ~RuleMatchingTest() override {
+    body_ = nullptr;
+    head_ = nullptr;
+    root_ = nullptr;
+    window_.reset();
+    EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
+  }
 
   void UpdateAllMatchingRules();
 
   scoped_refptr<cssom::CSSStyleSheet> GetDocumentStyleSheet(
       unsigned int index) {
-    return document_->style_sheets()->Item(index)->AsCSSStyleSheet();
+    return document()->style_sheets()->Item(index)->AsCSSStyleSheet();
   }
 
-  testing::StubEnvironmentSettings environment_settings_;
-  std::unique_ptr<css_parser::Parser> css_parser_;
+  void ExpectAndRemoveUserAgentStyleSheetRule(
+      cssom::RulesWithCascadePrecedence* matching_rules,
+      unsigned int index = 0) {
+    // Expecting the user agent style sheet style to be the first matching rule.
+    ASSERT_GT(matching_rules->size(), index);
+    ASSERT_EQ(
+        "address, blockquote, center, div, figure, figcaption, footer, form, "
+        "header, hr, legend, listing, p, plaintext, pre, xmp",
+        (*matching_rules)[index].first->selector_text());
+    ASSERT_EQ("display: block;",
+              (*matching_rules)[index].first->css_text(nullptr));
+    // Remove the user agent style sheet style from the matching rules.
+    matching_rules->erase(matching_rules->begin() + index);
+  }
+
+
+  std::unique_ptr<testing::StubWindow> window_;
   std::unique_ptr<dom_parser::Parser> dom_parser_;
   std::unique_ptr<DomStatTracker> dom_stat_tracker_;
-  HTMLElementContext html_element_context_;
+  std::unique_ptr<HTMLElementContext> html_element_context_;
 
-  scoped_refptr<Document> document_;
   scoped_refptr<HTMLElement> root_;
   scoped_refptr<HTMLElement> head_;
   scoped_refptr<HTMLElement> body_;
 };
 
 void RuleMatchingTest::UpdateAllMatchingRules() {
-  document_->UpdateSelectorTree();
-  NodeDescendantsIterator iterator(document_);
+  document()->UpdateSelectorTree();
+  NodeDescendantsIterator iterator(document());
   Node* child = iterator.First();
   while (child) {
     if (child->AsElement()) {
@@ -115,6 +145,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules, 1);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -128,6 +159,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -141,6 +173,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -154,6 +187,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -167,6 +201,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -181,6 +216,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(1),
             (*matching_rules)[0].first);
@@ -195,6 +231,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(2),
             (*matching_rules)[0].first);
@@ -208,6 +245,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -221,6 +259,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 }
 
@@ -232,6 +271,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -245,6 +285,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -258,6 +299,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -271,6 +313,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -284,17 +327,15 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
 }
 
 // div:focus should match focused div.
 TEST_F(RuleMatchingTest, FocusPseudoClassMatch) {
-  // Give the document browsing context which is needed for focus to work.
-  testing::StubWindow window;
-  document_->set_window(window.window());
   // Give the document initial computed style.
   ViewportSize view_size(320, 240);
-  document_->SetViewport(view_size);
+  document()->SetViewport(view_size);
 
   head_->set_inner_html("<style>:focus {}</style>");
   body_->set_inner_html("<div tabIndex=\"-1\"/>");
@@ -306,6 +347,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -313,12 +355,9 @@
 
 // div:focus shouldn't match unfocused div.
 TEST_F(RuleMatchingTest, FocusPseudoClassNoMatch) {
-  // Give the document browsing context which is needed for focus to work.
-  testing::StubWindow window;
-  document_->set_window(window.window());
   // Give the document initial computed style.
   ViewportSize view_size(320, 240);
-  document_->SetViewport(view_size);
+  document()->SetViewport(view_size);
 
   head_->set_inner_html("<style>:focus {}</style>");
   body_->set_inner_html("<div tabIndex=\"-1\"/>");
@@ -326,6 +365,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
 }
 
@@ -337,6 +377,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -350,6 +391,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
 }
 
@@ -364,6 +406,7 @@
   ASSERT_EQ(2, GetDocumentStyleSheet(0)->css_rules_same_origin()->length());
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 }
 
@@ -377,6 +420,7 @@
   ASSERT_EQ(2, GetDocumentStyleSheet(0)->css_rules_same_origin()->length());
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -392,6 +436,7 @@
   ASSERT_EQ(2, GetDocumentStyleSheet(0)->css_rules_same_origin()->length());
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(1),
             (*matching_rules)[0].first);
@@ -408,6 +453,7 @@
   ASSERT_EQ(2, GetDocumentStyleSheet(0)->css_rules_same_origin()->length());
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(2, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -424,6 +470,7 @@
   HTMLElement* html_element = body_->first_element_child()->AsHTMLElement();
   cssom::RulesWithCascadePrecedence* matching_rules =
       html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kAfterPseudoElementType));
   matching_rules =
@@ -452,6 +499,7 @@
   HTMLElement* html_element = body_->first_element_child()->AsHTMLElement();
   cssom::RulesWithCascadePrecedence* matching_rules =
       html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kAfterPseudoElementType));
   matching_rules =
@@ -483,6 +531,7 @@
   HTMLElement* html_element = body_->first_element_child()->AsHTMLElement();
   cssom::RulesWithCascadePrecedence* matching_rules =
       html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kBeforePseudoElementType));
   matching_rules =
@@ -511,6 +560,7 @@
   HTMLElement* html_element = body_->first_element_child()->AsHTMLElement();
   cssom::RulesWithCascadePrecedence* matching_rules =
       html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kBeforePseudoElementType));
   matching_rules =
@@ -541,6 +591,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
 }
 
@@ -552,6 +603,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -565,6 +617,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   matching_rules =
       body_->last_element_child()->AsHTMLElement()->matching_rules();
@@ -691,6 +744,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(3, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -723,6 +777,7 @@
           ->last_element_child()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -735,12 +790,11 @@
       "  <div id='div3'/>"
       "</div>");
 
-  scoped_refptr<Element> div1 =
-      QuerySelector(document_, "div", css_parser_.get());
+  scoped_refptr<Element> div1 = QuerySelector(document(), "div", css_parser());
   ASSERT_TRUE(div1);
   EXPECT_EQ("div1", div1->id());
 
-  EXPECT_FALSE(QuerySelector(document_, "span", css_parser_.get()));
+  EXPECT_FALSE(QuerySelector(document(), "span", css_parser()));
 }
 
 TEST_F(RuleMatchingTest, QuerySelectorShouldLimitResultInSubtree) {
@@ -750,12 +804,11 @@
       "  <div id='div3'/>"
       "</div>");
 
-  scoped_refptr<Element> div1 =
-      QuerySelector(document_, "div", css_parser_.get());
+  scoped_refptr<Element> div1 = QuerySelector(document(), "div", css_parser());
   ASSERT_TRUE(div1);
   EXPECT_EQ("div1", div1->id());
 
-  scoped_refptr<Element> div2 = QuerySelector(div1, "div", css_parser_.get());
+  scoped_refptr<Element> div2 = QuerySelector(div1, "div", css_parser());
   ASSERT_TRUE(div2);
   EXPECT_EQ("div2", div2->id());
 }
@@ -769,11 +822,10 @@
       "</div>");
 
   scoped_refptr<Element> div1 =
-      QuerySelector(document_, "#div1", css_parser_.get());
+      QuerySelector(document(), "#div1", css_parser());
   ASSERT_TRUE(div1);
   EXPECT_EQ("div1", div1->id());
-  scoped_refptr<Element> span =
-      QuerySelector(div1, ".out span", css_parser_.get());
+  scoped_refptr<Element> span = QuerySelector(div1, ".out span", css_parser());
   EXPECT_TRUE(span);
 }
 
@@ -786,7 +838,7 @@
       "<span/>");
 
   scoped_refptr<Element> span =
-      QuerySelector(document_, "div + div ~ span", css_parser_.get());
+      QuerySelector(document(), "div + div ~ span", css_parser());
   EXPECT_TRUE(span);
 }
 
@@ -804,7 +856,7 @@
       "</div>");
 
   scoped_refptr<Element> div = QuerySelector(
-      document_, "div ~ span + div ~ div + div > div + div", css_parser_.get());
+      document(), "div ~ span + div ~ div + div > div + div", css_parser());
   EXPECT_TRUE(div);
 }
 
@@ -816,18 +868,18 @@
       "</div>");
 
   scoped_refptr<NodeList> node_list;
-  node_list = QuerySelectorAll(document_, "div", css_parser_.get());
+  node_list = QuerySelectorAll(document(), "div", css_parser());
   ASSERT_EQ(3, node_list->length());
   EXPECT_EQ("div1", node_list->Item(0)->AsElement()->id());
   EXPECT_EQ("div2", node_list->Item(1)->AsElement()->id());
   EXPECT_EQ("div3", node_list->Item(2)->AsElement()->id());
 
-  node_list = QuerySelectorAll(document_, "span", css_parser_.get());
+  node_list = QuerySelectorAll(document(), "span", css_parser());
   EXPECT_EQ(0, node_list->length());
 }
 
 TEST_F(RuleMatchingTest, ElementMatches) {
-  scoped_refptr<Element> root = new Element(document_, base::Token("root"));
+  scoped_refptr<Element> root = new Element(document(), base::Token("root"));
   StrictMock<MockExceptionState> exception_state;
   EXPECT_TRUE(root->Matches("root", &exception_state));
   EXPECT_FALSE(root->Matches("r", &exception_state));
@@ -842,6 +894,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 }
 
@@ -854,16 +907,17 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules, 1);
   ASSERT_EQ(1, matching_rules->size());
 }
 
 TEST_F(RuleMatchingTest, StyleElementReorderingOneMatching) {
   scoped_refptr<HTMLElement> div1 =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   div1->set_inner_html("<style/>");
 
   scoped_refptr<HTMLElement> div2 =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   div2->set_inner_html("<style>* {}</style>");
 
   body_->set_inner_html("<div/>");
@@ -874,6 +928,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       head_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules, 1);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_NE(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -887,6 +942,7 @@
 
   matching_rules =
       head_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules, 1);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -896,11 +952,11 @@
 
 TEST_F(RuleMatchingTest, StyleElementReorderingTwoMatching) {
   scoped_refptr<HTMLElement> div1 =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   div1->set_inner_html("<style>* {}</style>");
 
   scoped_refptr<HTMLElement> div2 =
-      document_->CreateElement("div")->AsHTMLElement();
+      document()->CreateElement("div")->AsHTMLElement();
   div2->set_inner_html("<style>* {}</style>");
 
   body_->set_inner_html("<div/>");
@@ -911,6 +967,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       head_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules, 2);
   ASSERT_EQ(2, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -924,6 +981,7 @@
 
   matching_rules =
       head_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules, 2);
   ASSERT_EQ(2, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -936,19 +994,22 @@
   head_->set_inner_html("<style>div:hover {}</style>");
   body_->set_inner_html("<div/>");
 
-  document_->SetIndicatedElement(body_->first_element_child()->AsHTMLElement());
+  document()->SetIndicatedElement(
+      body_->first_element_child()->AsHTMLElement());
 
   UpdateAllMatchingRules();
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
 
-  document_->SetIndicatedElement(NULL);
+  document()->SetIndicatedElement(NULL);
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(0, matching_rules->size());
 }
@@ -962,11 +1023,14 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 
-  document_->SetIndicatedElement(body_->first_element_child()->AsHTMLElement());
+  document()->SetIndicatedElement(
+      body_->first_element_child()->AsHTMLElement());
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
@@ -978,20 +1042,22 @@
   head_->set_inner_html("<style>div:hover {}</style>");
   body_->set_inner_html("<div><span/></div>");
 
-  document_->SetIndicatedElement(
+  document()->SetIndicatedElement(
       body_->first_element_child()->first_element_child()->AsHTMLElement());
 
   UpdateAllMatchingRules();
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
 
-  document_->SetIndicatedElement(NULL);
+  document()->SetIndicatedElement(NULL);
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(0, matching_rules->size());
 }
@@ -1005,12 +1071,14 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 
-  document_->SetIndicatedElement(
+  document()->SetIndicatedElement(
       body_->first_element_child()->first_element_child()->AsHTMLElement());
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
@@ -1026,11 +1094,13 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 
   body_->first_element_child()->AsHTMLElement()->set_class_name("my-class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
@@ -1046,6 +1116,7 @@
 
   cssom::RulesWithCascadePrecedence* matching_rules =
       body_->first_element_child()->AsHTMLElement()->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -1053,6 +1124,7 @@
   body_->first_element_child()->AsHTMLElement()->RemoveAttribute("class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(0, matching_rules->size());
 }
@@ -1069,11 +1141,13 @@
           ->first_element_child()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 
   body_->first_element_child()->AsHTMLElement()->set_class_name("my-class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
@@ -1092,6 +1166,7 @@
           ->first_element_child()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules, 1);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -1099,6 +1174,7 @@
   body_->first_element_child()->AsHTMLElement()->RemoveAttribute("class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(0, matching_rules->size());
 }
@@ -1115,11 +1191,13 @@
           ->first_element_child()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 
   body_->first_element_child()->AsHTMLElement()->set_class_name("my-class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
@@ -1138,6 +1216,7 @@
           ->first_element_child()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -1145,6 +1224,7 @@
   body_->first_element_child()->AsHTMLElement()->RemoveAttribute("class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(0, matching_rules->size());
 }
@@ -1161,11 +1241,13 @@
           ->next_element_sibling()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 
   body_->first_element_child()->AsHTMLElement()->set_class_name("my-class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
@@ -1184,6 +1266,7 @@
           ->next_element_sibling()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -1191,6 +1274,7 @@
   body_->first_element_child()->AsHTMLElement()->RemoveAttribute("class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(0, matching_rules->size());
 }
@@ -1207,11 +1291,13 @@
           ->next_element_sibling()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(0, matching_rules->size());
 
   body_->first_element_child()->AsHTMLElement()->set_class_name("my-class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
@@ -1231,6 +1317,7 @@
           ->next_element_sibling()
           ->AsHTMLElement()
           ->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   ASSERT_EQ(1, matching_rules->size());
   EXPECT_EQ(GetDocumentStyleSheet(0)->css_rules_same_origin()->Item(0),
             (*matching_rules)[0].first);
@@ -1238,6 +1325,7 @@
   body_->first_element_child()->AsHTMLElement()->RemoveAttribute("class");
 
   UpdateAllMatchingRules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
 
   ASSERT_EQ(0, matching_rules->size());
 }
@@ -1254,6 +1342,7 @@
   HTMLElement* html_element = body_->first_element_child()->AsHTMLElement();
   cssom::RulesWithCascadePrecedence* matching_rules =
       html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kAfterPseudoElementType));
   matching_rules =
@@ -1267,6 +1356,7 @@
   UpdateAllMatchingRules();
 
   matching_rules = html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kAfterPseudoElementType));
   matching_rules =
@@ -1289,6 +1379,7 @@
   HTMLElement* html_element = body_->first_element_child()->AsHTMLElement();
   cssom::RulesWithCascadePrecedence* matching_rules =
       html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kAfterPseudoElementType));
   matching_rules =
@@ -1302,6 +1393,7 @@
   UpdateAllMatchingRules();
 
   matching_rules = html_element->matching_rules();
+  ExpectAndRemoveUserAgentStyleSheetRule(matching_rules);
   EXPECT_EQ(0, matching_rules->size());
   ASSERT_TRUE(html_element->pseudo_element(kAfterPseudoElementType));
   matching_rules =
diff --git a/cobalt/dom/source_buffer.cc b/cobalt/dom/source_buffer.cc
index 1f6278b..449eaa1 100644
--- a/cobalt/dom/source_buffer.cc
+++ b/cobalt/dom/source_buffer.cc
@@ -103,6 +103,19 @@
   return std::max<int>(bytes, 0);
 }
 
+size_t GetMaxAppendSizeInBytes(script::EnvironmentSettings* settings,
+                               size_t default_value) {
+  DOMSettings* dom_settings =
+      base::polymorphic_downcast<DOMSettings*>(settings);
+  DCHECK(dom_settings);
+  DCHECK(dom_settings->media_source_settings());
+  int bytes = dom_settings->media_source_settings()
+                  ->GetMaxSourceBufferAppendSizeInBytes()
+                  .value_or(default_value);
+  DCHECK_GT(bytes, 0);
+  return bytes;
+}
+
 }  // namespace
 
 SourceBuffer::OnInitSegmentReceivedHelper::OnInitSegmentReceivedHelper(
@@ -133,13 +146,13 @@
 
 SourceBuffer::SourceBuffer(script::EnvironmentSettings* settings,
                            const std::string& id, MediaSource* media_source,
-                           bool asynchronous_reduction_enabled,
                            ChunkDemuxer* chunk_demuxer, EventQueue* event_queue)
     : web::EventTarget(settings),
       on_init_segment_received_helper_(new OnInitSegmentReceivedHelper(this)),
       id_(id),
-      asynchronous_reduction_enabled_(asynchronous_reduction_enabled),
       evict_extra_in_bytes_(GetEvictExtraInBytes(settings)),
+      max_append_buffer_size_(
+          GetMaxAppendSizeInBytes(settings, kDefaultMaxAppendBufferSize)),
       media_source_(media_source),
       chunk_demuxer_(chunk_demuxer),
       event_queue_(event_queue),
@@ -154,6 +167,7 @@
   DCHECK(event_queue);
 
   LOG(INFO) << "Evict extra in bytes is set to " << evict_extra_in_bytes_;
+  LOG(INFO) << "Max append size in bytes is set to " << max_append_buffer_size_;
 
   chunk_demuxer_->SetTracksWatcher(
       id_,
@@ -381,10 +395,7 @@
   std::unique_ptr<SourceBufferAlgorithm> algorithm(
       new SourceBufferRemoveAlgorithm(
           chunk_demuxer_, id_, DoubleToTimeDelta(start), DoubleToTimeDelta(end),
-          base::Bind(asynchronous_reduction_enabled_
-                         ? &SourceBuffer::ScheduleAndMaybeDispatchImmediately
-                         : &SourceBuffer::ScheduleEvent,
-                     base::Unretained(this)),
+          base::Bind(&SourceBuffer::ScheduleEvent, base::Unretained(this)),
           base::Bind(&SourceBuffer::OnAlgorithmFinalized,
                      base::Unretained(this))));
   auto algorithm_runner =
@@ -481,16 +492,6 @@
   event_queue_->Enqueue(event);
 }
 
-void SourceBuffer::ScheduleAndMaybeDispatchImmediately(base::Token event_name) {
-  ScheduleEvent(event_name);
-  // TODO(b/244773734): Re-enable direct event dispatching
-  /*
-  scoped_refptr<web::Event> event = new web::Event(event_name);
-  event->set_target(this);
-  event_queue_->EnqueueAndMaybeDispatchImmediately(event);
-  */
-}
-
 bool SourceBuffer::PrepareAppend(size_t new_data_size,
                                  script::ExceptionState* exception_state) {
   TRACE_EVENT1("cobalt::dom", "SourceBuffer::PrepareAppend()", "new_data_size",
@@ -555,15 +556,12 @@
   std::unique_ptr<SourceBufferAlgorithm> algorithm(
       new SourceBufferAppendAlgorithm(
           media_source_, chunk_demuxer_, id_, pending_append_data_.get(), size,
-          DoubleToTimeDelta(append_window_start_),
+          max_append_buffer_size_, DoubleToTimeDelta(append_window_start_),
           DoubleToTimeDelta(append_window_end_),
           DoubleToTimeDelta(timestamp_offset_),
           base::Bind(&SourceBuffer::UpdateTimestampOffset,
                      base::Unretained(this)),
-          base::Bind(asynchronous_reduction_enabled_
-                         ? &SourceBuffer::ScheduleAndMaybeDispatchImmediately
-                         : &SourceBuffer::ScheduleEvent,
-                     base::Unretained(this)),
+          base::Bind(&SourceBuffer::ScheduleEvent, base::Unretained(this)),
           base::Bind(&SourceBuffer::OnAlgorithmFinalized,
                      base::Unretained(this)),
           &metrics_));
diff --git a/cobalt/dom/source_buffer.h b/cobalt/dom/source_buffer.h
index c7336e6..25b15ac 100644
--- a/cobalt/dom/source_buffer.h
+++ b/cobalt/dom/source_buffer.h
@@ -90,8 +90,8 @@
   // Custom, not in any spec.
   //
   SourceBuffer(script::EnvironmentSettings* settings, const std::string& id,
-               MediaSource* media_source, bool asynchronous_reduction_enabled,
-               ChunkDemuxer* chunk_demuxer, EventQueue* event_queue);
+               MediaSource* media_source, ChunkDemuxer* chunk_demuxer,
+               EventQueue* event_queue);
 
   // Web API: SourceBuffer
   //
@@ -167,7 +167,6 @@
 
   void OnInitSegmentReceived(std::unique_ptr<MediaTracks> tracks);
   void ScheduleEvent(base::Token event_name);
-  void ScheduleAndMaybeDispatchImmediately(base::Token event_name);
   bool PrepareAppend(size_t new_data_size,
                      script::ExceptionState* exception_state);
   void AppendBufferInternal(const unsigned char* data, size_t size,
@@ -185,10 +184,12 @@
       const std::string& track_type,
       const std::string& byte_stream_track_id) const;
 
+  static const size_t kDefaultMaxAppendBufferSize = 128 * 1024;
+
   scoped_refptr<OnInitSegmentReceivedHelper> on_init_segment_received_helper_;
   const std::string id_;
-  const bool asynchronous_reduction_enabled_;
   const size_t evict_extra_in_bytes_;
+  const size_t max_append_buffer_size_;
 
   MediaSource* media_source_;
   ChunkDemuxer* chunk_demuxer_;
diff --git a/cobalt/dom/source_buffer_algorithm.cc b/cobalt/dom/source_buffer_algorithm.cc
index 50eec01..a24e51c 100644
--- a/cobalt/dom/source_buffer_algorithm.cc
+++ b/cobalt/dom/source_buffer_algorithm.cc
@@ -27,8 +27,8 @@
 SourceBufferAppendAlgorithm::SourceBufferAppendAlgorithm(
     MediaSource* media_source, ::media::ChunkDemuxer* chunk_demuxer,
     const std::string& id, const uint8_t* buffer, size_t size_in_bytes,
-    base::TimeDelta append_window_start, base::TimeDelta append_window_end,
-    base::TimeDelta timestamp_offset,
+    size_t max_append_size_in_bytes, base::TimeDelta append_window_start,
+    base::TimeDelta append_window_end, base::TimeDelta timestamp_offset,
     UpdateTimestampOffsetCB&& update_timestamp_offset_cb,
     ScheduleEventCB&& schedule_event_cb, base::OnceClosure&& finalized_cb,
     SourceBufferMetrics* metrics)
@@ -36,6 +36,7 @@
       chunk_demuxer_(chunk_demuxer),
       id_(id),
       buffer_(buffer),
+      max_append_size_(max_append_size_in_bytes),
       bytes_remaining_(size_in_bytes),
       append_window_start_(append_window_start),
       append_window_end_(append_window_end),
@@ -46,6 +47,7 @@
       metrics_(metrics) {
   DCHECK(media_source_);
   DCHECK(chunk_demuxer_);
+  DCHECK_GT(static_cast<int>(max_append_size_), 0);
   if (bytes_remaining_ > 0) {
     DCHECK(buffer);
   }
@@ -60,9 +62,7 @@
   DCHECK(finished);
   DCHECK(!*finished);
 
-  const size_t kMaxAppendSize = 128 * 1024;
-
-  size_t append_size = std::min(bytes_remaining_, kMaxAppendSize);
+  size_t append_size = std::min(bytes_remaining_, max_append_size_);
 
   TRACE_EVENT1("cobalt::dom", "SourceBufferAppendAlgorithm::Process()",
                "append_size", append_size);
diff --git a/cobalt/dom/source_buffer_algorithm.h b/cobalt/dom/source_buffer_algorithm.h
index d9e0f16..3505794 100644
--- a/cobalt/dom/source_buffer_algorithm.h
+++ b/cobalt/dom/source_buffer_algorithm.h
@@ -58,8 +58,8 @@
   SourceBufferAppendAlgorithm(
       MediaSource* media_source, ::media::ChunkDemuxer* chunk_demuxer,
       const std::string& id, const uint8_t* buffer, size_t size_in_bytes,
-      base::TimeDelta append_window_start, base::TimeDelta append_window_end,
-      base::TimeDelta timestamp_offset,
+      size_t max_append_size_in_bytes, base::TimeDelta append_window_start,
+      base::TimeDelta append_window_end, base::TimeDelta timestamp_offset,
       UpdateTimestampOffsetCB&& update_timestamp_offset_cb,
       ScheduleEventCB&& schedule_event_cb, base::OnceClosure&& finalized_cb,
       SourceBufferMetrics* metrics);
@@ -74,6 +74,7 @@
   ::media::ChunkDemuxer* const chunk_demuxer_;
   const std::string id_;
   const uint8_t* buffer_;
+  const size_t max_append_size_;
   size_t bytes_remaining_;
   const base::TimeDelta append_window_start_;
   const base::TimeDelta append_window_end_;
diff --git a/cobalt/dom/testing/fake_document.h b/cobalt/dom/testing/fake_document.h
index 3ab3d94..494da4e 100644
--- a/cobalt/dom/testing/fake_document.h
+++ b/cobalt/dom/testing/fake_document.h
@@ -55,7 +55,7 @@
         options.csp_insecure_allowed_token);
   }
 
-  web::CspDelegate* csp_delegate() const override {
+  web::CspDelegate* GetCSPDelegate() const override {
     return csp_delegate_.get();
   }
 
diff --git a/cobalt/dom/testing/stub_window.h b/cobalt/dom/testing/stub_window.h
index 6651633..34a2d94 100644
--- a/cobalt/dom/testing/stub_window.h
+++ b/cobalt/dom/testing/stub_window.h
@@ -24,6 +24,7 @@
 #include "base/optional.h"
 #include "cobalt/base/debugger_hooks.h"
 #include "cobalt/css_parser/parser.h"
+#include "cobalt/cssom/css_parser.h"
 #include "cobalt/cssom/viewport_size.h"
 #include "cobalt/dom/captions/system_caption_settings.h"
 #include "cobalt/dom/dom_settings.h"
@@ -68,7 +69,7 @@
     return web_context_.get();
   }
 
-  scoped_refptr<dom::Window> window() {
+  const scoped_refptr<dom::Window>& window() {
     if (!window_) InitializeWindow();
     return window_;
   }
@@ -78,7 +79,7 @@
     if (!window_) InitializeWindow();
     return web_context()->global_environment();
   }
-  css_parser::Parser* css_parser() { return css_parser_.get(); }
+  cssom::CSSParser* css_parser() { return css_parser_.get(); }
   web::EnvironmentSettings* environment_settings() {
     // The Window has to be set as the global object for the environment
     // settings object to be valid.
@@ -91,17 +92,7 @@
     on_screen_keyboard_bridge_ = on_screen_keyboard_bridge;
   }
 
- private:
-  static void StubLoadCompleteCallback(
-      const base::Optional<std::string>& error) {}
-
-  void InitializeWebContext() {
-    web_context_.reset(new web::testing::StubWebContext());
-    web_context()->setup_environment_settings(
-        new dom::testing::StubEnvironmentSettings(options_));
-    web_context()->environment_settings()->set_creation_url(
-        GURL("about:blank"));
-  }
+  void set_css_parser(cssom::CSSParser* parser) { css_parser_.reset(parser); }
 
   void InitializeWindow() {
     loader_factory_.reset(new loader::LoaderFactory(
@@ -110,7 +101,9 @@
         base::ThreadPriority::DEFAULT));
     system_caption_settings_ = new cobalt::dom::captions::SystemCaptionSettings(
         web_context()->environment_settings());
-    css_parser_.reset(css_parser::Parser::Create().release());
+    if (!css_parser_) {
+      css_parser_.reset(css_parser::Parser::Create().release());
+    }
     dom_parser_.reset(
         new dom_parser::Parser(base::Bind(&StubLoadCompleteCallback)));
     dom_stat_tracker_.reset(new dom::DomStatTracker("StubWindow"));
@@ -143,9 +136,22 @@
         window_, web_context()->environment_settings());
   }
 
+ private:
+  static void StubLoadCompleteCallback(
+      const base::Optional<std::string>& error) {}
+
+  void InitializeWebContext() {
+    web_context_.reset(new web::testing::StubWebContext());
+    web_context()->setup_environment_settings(
+        new dom::testing::StubEnvironmentSettings(options_));
+    web_context()->environment_settings()->set_creation_url(
+        GURL("about:blank"));
+  }
+
+
   OnScreenKeyboardBridge* on_screen_keyboard_bridge_ = nullptr;
   DOMSettings::Options options_;
-  std::unique_ptr<css_parser::Parser> css_parser_;
+  std::unique_ptr<cssom::CSSParser> css_parser_;
   std::unique_ptr<dom_parser::Parser> dom_parser_;
   std::unique_ptr<loader::LoaderFactory> loader_factory_;
   dom::LocalStorageDatabase local_storage_database_;
diff --git a/cobalt/dom/testing/test_with_javascript.h b/cobalt/dom/testing/test_with_javascript.h
index 30204af..e51747f 100644
--- a/cobalt/dom/testing/test_with_javascript.h
+++ b/cobalt/dom/testing/test_with_javascript.h
@@ -42,24 +42,38 @@
     EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
   }
 
-  StubWindow* stub_window() { return stub_window_.get(); }
-  web::testing::StubWebContext* stub_web_context() {
+  StubWindow* stub_window() const { return stub_window_.get(); }
+  web::testing::StubWebContext* stub_web_context() const {
+    DCHECK(stub_window_);
     return stub_window_->web_context();
   }
 
-  Window* window() { return stub_window_->window().get(); }
+  Window* window() const {
+    DCHECK(stub_window_);
+    return stub_window_->window().get();
+  }
 
-  bool EvaluateScript(const std::string& js_code, std::string* result) {
+  web::Context* web_context() const { return stub_web_context(); }
+
+  scoped_refptr<script::GlobalEnvironment> global_environment() const {
+    DCHECK(web_context());
+    return web_context()->global_environment();
+  }
+
+  bool EvaluateScript(const std::string& js_code,
+                      std::string* result = nullptr) {
+    DCHECK(global_environment());
     return global_environment()->EvaluateScript(
         CreateSourceCodeAndPrepareEval(js_code), result);
   }
 
   bool EvaluateScript(
       const std::string& js_code,
-      const scoped_refptr<script::Wrappable>& owning_object,
-      base::Optional<script::ValueHandleHolder::Reference>* result = NULL) {
+      base::Optional<script::ValueHandleHolder::Reference>* result) {
+    DCHECK(global_environment());
     return global_environment()->EvaluateScript(
-        CreateSourceCodeAndPrepareEval(js_code), owning_object, result);
+        CreateSourceCodeAndPrepareEval(js_code),
+        web_context()->GetWindowOrWorkerGlobalScope(), result);
   }
 
   ::testing::StrictMock<script::testing::MockExceptionState>*
diff --git a/cobalt/dom/window.cc b/cobalt/dom/window.cc
index 0284726..c5d5ac0 100644
--- a/cobalt/dom/window.cc
+++ b/cobalt/dom/window.cc
@@ -84,7 +84,7 @@
 };
 
 Window::Window(
-    script::EnvironmentSettings* settings, const ViewportSize& view_size,
+    web::EnvironmentSettings* settings, const ViewportSize& view_size,
     base::ApplicationState initial_application_state,
     cssom::CSSParser* css_parser, Parser* dom_parser,
     loader::FetcherFactory* fetcher_factory,
@@ -152,14 +152,6 @@
           initial_application_state, synchronous_loader_interrupt,
           performance_.get(), enable_inline_script_warnings,
           video_playback_rate_multiplier)),
-      ALLOW_THIS_IN_INITIALIZER_LIST(document_(new Document(
-          html_element_context(),
-          Document::Options(
-              settings->creation_url(), this,
-              base::Bind(&Window::FireHashChangeEvent, base::Unretained(this)),
-              performance_->timing()->GetNavigationStartClock(),
-              navigation_callback, ParseUserAgentStyleSheet(css_parser),
-              view_size, cookie_jar, dom_max_element_depth)))),
       document_loader_(nullptr),
       history_(new History()),
       navigator_(new Navigator(environment_settings(), captions)),
@@ -191,6 +183,16 @@
       screenshot_manager_(settings, screenshot_function_callback),
       ui_nav_root_(ui_nav_root),
       enable_map_to_mesh_(enable_map_to_mesh) {
+  document_ = new Document(
+      html_element_context(),
+      Document::Options(
+          settings->creation_url(),
+          base::Bind(&Window::FireHashChangeEvent, base::Unretained(this)),
+          performance_->timing()->GetNavigationStartClock(),
+          navigation_callback, ParseUserAgentStyleSheet(css_parser), view_size,
+          cookie_jar, dom_max_element_depth),
+      csp_delegate());
+
   set_navigator_base(navigator_);
   document_->AddObserver(relay_on_load_event_.get());
   html_element_context()->application_lifecycle_state()->AddObserver(this);
diff --git a/cobalt/dom/window.h b/cobalt/dom/window.h
index 033ac82..73688a1 100644
--- a/cobalt/dom/window.h
+++ b/cobalt/dom/window.h
@@ -59,13 +59,13 @@
 #include "cobalt/network_bridge/cookie_jar.h"
 #include "cobalt/network_bridge/net_poster.h"
 #include "cobalt/script/callback_function.h"
-#include "cobalt/script/environment_settings.h"
 #include "cobalt/script/error_report.h"
 #include "cobalt/script/execution_state.h"
 #include "cobalt/script/script_runner.h"
 #include "cobalt/script/script_value_factory.h"
 #include "cobalt/ui_navigation/nav_item.h"
 #include "cobalt/web/csp_delegate_type.h"
+#include "cobalt/web/environment_settings.h"
 #include "cobalt/web/event_target.h"
 #include "cobalt/web/url_registry.h"
 #include "cobalt/web/user_agent_platform_info.h"
@@ -131,8 +131,7 @@
   };
 
   Window(
-      script::EnvironmentSettings* settings,
-      const cssom::ViewportSize& view_size,
+      web::EnvironmentSettings* settings, const cssom::ViewportSize& view_size,
       base::ApplicationState initial_application_state,
       cssom::CSSParser* css_parser, Parser* dom_parser,
       loader::FetcherFactory* fetcher_factory,
diff --git a/cobalt/dom/xml_document_test.cc b/cobalt/dom/xml_document_test.cc
index 625d6fb..7adeaf1 100644
--- a/cobalt/dom/xml_document_test.cc
+++ b/cobalt/dom/xml_document_test.cc
@@ -14,19 +14,65 @@
 
 #include "cobalt/dom/xml_document.h"
 
+#include <memory>
+
 #include "base/memory/ref_counted.h"
 #include "cobalt/dom/document.h"
+#include "cobalt/dom/global_stats.h"
 #include "cobalt/dom/html_element_context.h"
+#include "cobalt/dom/testing/stub_window.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 namespace cobalt {
 namespace dom {
 
+class XMLDocumentTestInWindow : public ::testing::Test {
+ protected:
+  XMLDocumentTestInWindow();
+  ~XMLDocumentTestInWindow() override;
+
+  std::unique_ptr<testing::StubWindow> window_;
+  std::unique_ptr<HTMLElementContext> html_element_context_;
+};
+
+XMLDocumentTestInWindow::XMLDocumentTestInWindow()
+    : window_(new testing::StubWindow) {
+  EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
+  window_->InitializeWindow();
+  html_element_context_.reset(new HTMLElementContext(
+      window_->web_context()->environment_settings(), NULL, NULL, NULL, NULL,
+      NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,
+      "", base::kApplicationStateStarted, NULL, NULL));
+}
+
+XMLDocumentTestInWindow::~XMLDocumentTestInWindow() {
+  window_.reset();
+  EXPECT_TRUE(GlobalStats::GetInstance()->CheckNoLeaks());
+}
+
 TEST(XMLDocumentTest, IsXMLDocument) {
   HTMLElementContext html_element_context;
   scoped_refptr<Document> document = new XMLDocument(&html_element_context);
   EXPECT_TRUE(document->IsXMLDocument());
 }
 
+TEST(XMLDocumentTest, HasNoBrowsingContext) {
+  HTMLElementContext html_element_context;
+  scoped_refptr<Document> document = new XMLDocument(&html_element_context);
+  EXPECT_FALSE(document->HasBrowsingContext());
+}
+
+TEST_F(XMLDocumentTestInWindow, IsXMLDocument) {
+  scoped_refptr<Document> document =
+      new XMLDocument(html_element_context_.get());
+  EXPECT_TRUE(document->IsXMLDocument());
+}
+
+TEST_F(XMLDocumentTestInWindow, HasNoBrowsingContext) {
+  scoped_refptr<Document> document =
+      new XMLDocument(html_element_context_.get());
+  EXPECT_FALSE(document->HasBrowsingContext());
+}
+
 }  // namespace dom
 }  // namespace cobalt
diff --git a/cobalt/dom_parser/html_decoder.cc b/cobalt/dom_parser/html_decoder.cc
index 742d4db..01267f3 100644
--- a/cobalt/dom_parser/html_decoder.cc
+++ b/cobalt/dom_parser/html_decoder.cc
@@ -60,7 +60,7 @@
   }
 
   csp::ResponseHeaders csp_headers(headers);
-  if (document_->csp_delegate()->OnReceiveHeaders(csp_headers) ||
+  if (document_->GetCSPDelegate()->OnReceiveHeaders(csp_headers) ||
       require_csp_ == csp::kCSPOptional) {
     return loader::kLoadResponseContinue;
   } else {
diff --git a/cobalt/fetch/fetch_internal.idl b/cobalt/fetch/fetch_internal.idl
index d665bf2..5f46c77 100644
--- a/cobalt/fetch/fetch_internal.idl
+++ b/cobalt/fetch/fetch_internal.idl
@@ -16,6 +16,8 @@
 // specific to the fetch polyfill and may change as the implementation changes.
 // This is not meant to be public and should not be used outside of the fetch
 // implementation.
+
+[Exposed=(Window,Worker)]
 interface FetchInternal {
   [CallWith=EnvironmentSettings] static boolean isUrlValid(
       DOMString url, boolean allowCredentials);
diff --git a/cobalt/h5vcc/h5vcc_storage.cc b/cobalt/h5vcc/h5vcc_storage.cc
index d734a0e..cecdd0c 100644
--- a/cobalt/h5vcc/h5vcc_storage.cc
+++ b/cobalt/h5vcc/h5vcc_storage.cc
@@ -74,6 +74,11 @@
   backend->DoomAllEntries(base::DoNothing());
 }
 
+void ClearCacheOfTypeHelper(disk_cache::ResourceType type,
+                            disk_cache::CobaltBackendImpl* backend) {
+  backend->DoomAllEntriesOfType(type, base::DoNothing());
+}
+
 }  // namespace
 
 H5vccStorage::H5vccStorage(
@@ -229,7 +234,8 @@
     H5vccStorageResourceTypeQuotaBytesDictionary quota) {
   if (!quota.has_other() || !quota.has_html() || !quota.has_css() ||
       !quota.has_image() || !quota.has_font() || !quota.has_splash() ||
-      !quota.has_uncompiled_js() || !quota.has_compiled_js()) {
+      !quota.has_uncompiled_js() || !quota.has_compiled_js() ||
+      !quota.has_cache_api()) {
     return SetQuotaResponse(
         "H5vccStorageResourceTypeQuotaBytesDictionary input parameter missing "
         "required fields.");
@@ -237,16 +243,17 @@
 
   if (quota.other() < 0 || quota.html() < 0 || quota.css() < 0 ||
       quota.image() < 0 || quota.font() < 0 || quota.splash() < 0 ||
-      quota.uncompiled_js() < 0 || quota.compiled_js() < 0) {
+      quota.uncompiled_js() < 0 || quota.compiled_js() < 0 ||
+      quota.cache_api() < 0) {
     return SetQuotaResponse(
         "H5vccStorageResourceTypeQuotaBytesDictionary input parameter fields "
-        "cannot "
-        "have a negative value.");
+        "cannot have a negative value.");
   }
 
   auto quota_total = quota.other() + quota.html() + quota.css() +
                      quota.image() + quota.font() + quota.splash() +
-                     quota.uncompiled_js() + quota.compiled_js();
+                     quota.uncompiled_js() + quota.compiled_js() +
+                     quota.cache_api();
 
   uint32_t max_quota_size = 24 * 1024 * 1024;
 #if SB_API_VERSION >= 14
@@ -282,6 +289,8 @@
                             static_cast<uint32_t>(quota.uncompiled_js()));
   SetAndSaveQuotaForBackend(disk_cache::kCompiledScript,
                             static_cast<uint32_t>(quota.compiled_js()));
+  SetAndSaveQuotaForBackend(disk_cache::kCacheApi,
+                            static_cast<uint32_t>(quota.cache_api()));
   return SetQuotaResponse("", true);
 }
 
@@ -289,6 +298,12 @@
                                              uint32_t bytes) {
   if (cache_backend_) {
     cache_backend_->UpdateSizes(type, bytes);
+
+    if (bytes == 0) {
+      network_module_->task_runner()->PostTask(
+          FROM_HERE, base::Bind(&ClearCacheOfTypeHelper, type,
+                                base::Unretained(cache_backend_)));
+    }
   }
   cobalt::cache::Cache::GetInstance()->Resize(type, bytes);
 }
@@ -312,6 +327,7 @@
       cobalt::cache::Cache::GetInstance()
           ->GetMaxCacheStorageInBytes(disk_cache::kCompiledScript)
           .value());
+  quota.set_cache_api(cache_backend_->GetQuota(disk_cache::kCacheApi));
 
   uint32_t max_quota_size = 24 * 1024 * 1024;
 #if SB_API_VERSION >= 14
diff --git a/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl b/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
index bb2677b..322f82d 100644
--- a/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
+++ b/cobalt/h5vcc/h5vcc_storage_resource_type_quota_bytes_dictionary.idl
@@ -21,5 +21,6 @@
   unsigned long splash;
   unsigned long uncompiled_js;
   unsigned long compiled_js;
+  unsigned long cache_api;
   unsigned long total;
 };
diff --git a/cobalt/layout_tests/testdata/BUILD.gn b/cobalt/layout_tests/testdata/BUILD.gn
index 172a734..5741156 100644
--- a/cobalt/layout_tests/testdata/BUILD.gn
+++ b/cobalt/layout_tests/testdata/BUILD.gn
@@ -1764,6 +1764,7 @@
     "web-platform-tests/mediasession/web_platform_tests.txt",
     "web-platform-tests/performance-timeline/web_platform_tests.txt",
     "web-platform-tests/resource-timing/web_platform_tests.txt",
+    "web-platform-tests/service-workers/web_platform_tests.txt",
     "web-platform-tests/streams/web_platform_tests.txt",
     "web-platform-tests/websockets/web_platform_tests.txt",
     "web-platform-tests/workers/web_platform_tests.txt",
diff --git a/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt b/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
new file mode 100644
index 0000000..ade7e63
--- /dev/null
+++ b/cobalt/layout_tests/testdata/web-platform-tests/service-workers/web_platform_tests.txt
@@ -0,0 +1,21 @@
+# Service Worker API tests
+
+cache-storage/common.https.html, PASS
+cache-storage/serviceworker/cache-add.https.html, DISABLE
+cache-storage/serviceworker/cache-delete.https.html, DISABLE
+cache-storage/serviceworker/cache-match.https.html, DISABLE
+cache-storage/serviceworker/cache-put.https.html, DISABLE
+cache-storage/serviceworker/cache-storage-match.https.html, DISABLE
+cache-storage/serviceworker/cache-storage.https.html, DISABLE
+cache-storage/worker/cache-add.https.html, PASS
+cache-storage/worker/cache-delete.https.html, PASS
+cache-storage/worker/cache-match.https.html, PASS
+cache-storage/worker/cache-put.https.html, PASS
+cache-storage/worker/cache-storage-match.https.html, PASS
+cache-storage/worker/cache-storage.https.html, PASS
+cache-storage/window/cache-add.https.html, PASS
+cache-storage/window/cache-delete.https.html, PASS
+cache-storage/window/cache-match.https.html, PASS
+cache-storage/window/cache-put.https.html, PASS
+cache-storage/window/cache-storage-match.https.html, PASS
+cache-storage/window/cache-storage.https.html, PASS
diff --git a/cobalt/layout_tests/web_platform_tests.cc b/cobalt/layout_tests/web_platform_tests.cc
index c5406bf..fda4f74 100644
--- a/cobalt/layout_tests/web_platform_tests.cc
+++ b/cobalt/layout_tests/web_platform_tests.cc
@@ -447,6 +447,11 @@
     ::testing::ValuesIn(EnumerateWebPlatformTests("resource-timing")),
     GetTestName());
 
+INSTANTIATE_TEST_CASE_P(
+    service_workers, WebPlatformTest,
+    ::testing::ValuesIn(EnumerateWebPlatformTests("service-workers")),
+    GetTestName());
+
 INSTANTIATE_TEST_CASE_P(streams, WebPlatformTest,
                         ::testing::ValuesIn(EnumerateWebPlatformTests(
                             "streams", "'ReadableStream' in this")),
diff --git a/cobalt/loader/fetcher_factory.cc b/cobalt/loader/fetcher_factory.cc
index ce60177..ac5fca0 100644
--- a/cobalt/loader/fetcher_factory.cc
+++ b/cobalt/loader/fetcher_factory.cc
@@ -17,6 +17,7 @@
 #include <memory>
 #include <sstream>
 #include <string>
+#include <utility>
 
 #include "base/bind.h"
 #include "base/files/file_path.h"
@@ -86,13 +87,15 @@
     const GURL& url, const disk_cache::ResourceType type,
     Fetcher::Handler* handler) {
   return CreateSecureFetcher(url, csp::SecurityCallback(), kNoCORSMode,
-                             Origin(), type, handler);
+                             Origin(), type, net::HttpRequestHeaders(),
+                             handler);
 }
 
 std::unique_ptr<Fetcher> FetcherFactory::CreateSecureFetcher(
     const GURL& url, const csp::SecurityCallback& url_security_callback,
     RequestMode request_mode, const Origin& origin,
-    const disk_cache::ResourceType type, Fetcher::Handler* handler) {
+    const disk_cache::ResourceType type, net::HttpRequestHeaders headers,
+    Fetcher::Handler* handler) {
   LOG(INFO) << "Fetching: " << ClipUrl(url, 200);
 
   if (!url.is_valid()) {
@@ -106,6 +109,7 @@
       network_module_) {
     NetFetcher::Options options;
     options.resource_type = type;
+    options.headers = std::move(headers);
     return std::unique_ptr<Fetcher>(
         new NetFetcher(url, url_security_callback, handler, network_module_,
                        options, request_mode, origin));
diff --git a/cobalt/loader/fetcher_factory.h b/cobalt/loader/fetcher_factory.h
index 4638e10..abfa85e 100644
--- a/cobalt/loader/fetcher_factory.h
+++ b/cobalt/loader/fetcher_factory.h
@@ -25,6 +25,7 @@
 #include "cobalt/loader/blob_fetcher.h"
 #include "cobalt/loader/fetcher.h"
 #include "net/disk_cache/cobalt/resource_type.h"
+#include "net/http/http_request_headers.h"
 #include "url/gurl.h"
 
 namespace cobalt {
@@ -56,7 +57,8 @@
   std::unique_ptr<Fetcher> CreateSecureFetcher(
       const GURL& url, const csp::SecurityCallback& url_security_callback,
       RequestMode request_mode, const Origin& origin,
-      const disk_cache::ResourceType type, Fetcher::Handler* handler);
+      const disk_cache::ResourceType type, net::HttpRequestHeaders headers,
+      Fetcher::Handler* handler);
 
   network::NetworkModule* network_module() const { return network_module_; }
 
diff --git a/cobalt/loader/loader_factory.cc b/cobalt/loader/loader_factory.cc
index ba571ba..0cb575e 100644
--- a/cobalt/loader/loader_factory.cc
+++ b/cobalt/loader/loader_factory.cc
@@ -71,7 +71,8 @@
 
   std::unique_ptr<Loader> loader(new Loader(
       fetcher_creator,
-      base::Bind(&loader::TextDecoder::Create, link_available_callback),
+      base::Bind(&loader::TextDecoder::Create, link_available_callback,
+                 loader::TextDecoder::ResponseStartedCallback()),
       load_complete_callback,
       base::Bind(&LoaderFactory::OnLoaderDestroyed, base::Unretained(this)),
       is_suspended_));
diff --git a/cobalt/loader/loader_test.cc b/cobalt/loader/loader_test.cc
index fc0b3b6..424f402 100644
--- a/cobalt/loader/loader_test.cc
+++ b/cobalt/loader/loader_test.cc
@@ -250,13 +250,13 @@
   FileFetcher::Options fetcher_options;
   TextDecoderCallback text_decoder_callback(&run_loop);
   LoaderCallback loader_callback(&run_loop);
-  Loader loader(
-      base::Bind(&FileFetcher::Create, file_path, fetcher_options),
-      base::Bind(&loader::TextDecoder::Create,
-                 base::Bind(&TextDecoderCallback::OnDone,
-                            base::Unretained(&text_decoder_callback))),
-      base::Bind(&LoaderCallback::OnLoadComplete,
-                 base::Unretained(&loader_callback)));
+  Loader loader(base::Bind(&FileFetcher::Create, file_path, fetcher_options),
+                base::Bind(&loader::TextDecoder::Create,
+                           base::Bind(&TextDecoderCallback::OnDone,
+                                      base::Unretained(&text_decoder_callback)),
+                           loader::TextDecoder::ResponseStartedCallback()),
+                base::Bind(&LoaderCallback::OnLoadComplete,
+                           base::Unretained(&loader_callback)));
 
   // When the message loop runs, the loader will start loading. It'll quit when
   // loading is finished.
diff --git a/cobalt/loader/net_fetcher.cc b/cobalt/loader/net_fetcher.cc
index 5c55904..c83c5b4 100644
--- a/cobalt/loader/net_fetcher.cc
+++ b/cobalt/loader/net_fetcher.cc
@@ -103,6 +103,9 @@
       origin_(origin),
       request_script_(options.resource_type == disk_cache::kUncompiledScript) {
   url_fetcher_ = net::URLFetcher::Create(url, options.request_method, this);
+  if (!options.headers.IsEmpty()) {
+    url_fetcher_->SetExtraRequestHeaders(options.headers.ToString());
+  }
   url_fetcher_->SetRequestContext(
       network_module->url_request_context_getter().get());
   std::unique_ptr<URLFetcherStringWriter> download_data_writer(
@@ -164,14 +167,13 @@
     }
   }
 
-  if (IsResponseCodeSuccess(source->GetResponseCode())) {
-    if (handler()->OnResponseStarted(this, source->GetResponseHeaders()) ==
-        kLoadResponseAbort) {
-      std::string msg(
-          base::StringPrintf("Handler::OnResponseStarted aborted URL %s",
-                             source->GetURL().spec().c_str()));
-      return HandleError(msg).InvalidateThis();
-    }
+  if ((handler()->OnResponseStarted(this, source->GetResponseHeaders()) ==
+       kLoadResponseAbort) ||
+      (!IsResponseCodeSuccess(source->GetResponseCode()))) {
+    std::string msg(
+        base::StringPrintf("Handler::OnResponseStarted aborted URL %s",
+                           source->GetURL().spec().c_str()));
+    return HandleError(msg).InvalidateThis();
   }
 
   // net::URLFetcher can not guarantee GetResponseHeaders() always return
diff --git a/cobalt/loader/net_fetcher.h b/cobalt/loader/net_fetcher.h
index 4fd1e80..4c4ec9b 100644
--- a/cobalt/loader/net_fetcher.h
+++ b/cobalt/loader/net_fetcher.h
@@ -27,6 +27,7 @@
 #include "cobalt/loader/fetcher.h"
 #include "cobalt/network/network_module.h"
 #include "net/disk_cache/cobalt/resource_type.h"
+#include "net/http/http_request_headers.h"
 #include "net/url_request/url_fetcher.h"
 #include "net/url_request/url_fetcher_delegate.h"
 #include "url/gurl.h"
@@ -44,6 +45,7 @@
           resource_type(disk_cache::kOther) {}
     net::URLFetcher::RequestType request_method;
     disk_cache::ResourceType resource_type;
+    net::HttpRequestHeaders headers;
   };
 
   NetFetcher(const GURL& url, const csp::SecurityCallback& security_callback,
diff --git a/cobalt/loader/script_loader_factory.cc b/cobalt/loader/script_loader_factory.cc
index 8fb5cac..d70f846 100644
--- a/cobalt/loader/script_loader_factory.cc
+++ b/cobalt/loader/script_loader_factory.cc
@@ -15,6 +15,7 @@
 #include "cobalt/loader/script_loader_factory.h"
 
 #include <memory>
+#include <utility>
 
 #include "base/threading/platform_thread.h"
 #include "cobalt/loader/image/threaded_image_decoder_proxy.h"
@@ -47,19 +48,32 @@
     const csp::SecurityCallback& url_security_callback,
     const TextDecoder::TextAvailableCallback& script_available_callback,
     const Loader::OnCompleteFunction& load_complete_callback) {
+  return CreateScriptLoader(
+      url, origin, url_security_callback, script_available_callback,
+      TextDecoder::ResponseStartedCallback(), load_complete_callback);
+}
+
+std::unique_ptr<Loader> ScriptLoaderFactory::CreateScriptLoader(
+    const GURL& url, const Origin& origin,
+    const csp::SecurityCallback& url_security_callback,
+    const TextDecoder::TextAvailableCallback& script_available_callback,
+    const TextDecoder::ResponseStartedCallback& response_started_callback,
+    const Loader::OnCompleteFunction& load_complete_callback,
+    net::HttpRequestHeaders headers) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
   Loader::FetcherCreator fetcher_creator =
       MakeFetcherCreator(url, url_security_callback, kNoCORSMode, origin,
-                         disk_cache::kUncompiledScript);
+                         disk_cache::kUncompiledScript, std::move(headers));
 
-  std::unique_ptr<Loader> loader(new Loader(
-      fetcher_creator,
-      base::Bind(&loader::TextDecoder::Create, script_available_callback),
-      load_complete_callback,
-      base::Bind(&ScriptLoaderFactory::OnLoaderDestroyed,
-                 base::Unretained(this)),
-      is_suspended_));
+  std::unique_ptr<Loader> loader(
+      new Loader(fetcher_creator,
+                 base::Bind(&TextDecoder::Create, script_available_callback,
+                            response_started_callback),
+                 load_complete_callback,
+                 base::Bind(&ScriptLoaderFactory::OnLoaderDestroyed,
+                            base::Unretained(this)),
+                 is_suspended_));
 
   OnLoaderCreated(loader.get());
   return loader;
@@ -68,12 +82,13 @@
 Loader::FetcherCreator ScriptLoaderFactory::MakeFetcherCreator(
     const GURL& url, const csp::SecurityCallback& url_security_callback,
     RequestMode request_mode, const Origin& origin,
-    disk_cache::ResourceType type) {
+    disk_cache::ResourceType type, net::HttpRequestHeaders headers) {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
 
   return base::Bind(&FetcherFactory::CreateSecureFetcher,
                     base::Unretained(fetcher_factory_), url,
-                    url_security_callback, request_mode, origin, type);
+                    url_security_callback, request_mode, origin, type,
+                    std::move(headers));
 }
 
 void ScriptLoaderFactory::OnLoaderCreated(Loader* loader) {
diff --git a/cobalt/loader/script_loader_factory.h b/cobalt/loader/script_loader_factory.h
index 5fcc8a4..18e0da4 100644
--- a/cobalt/loader/script_loader_factory.h
+++ b/cobalt/loader/script_loader_factory.h
@@ -27,6 +27,7 @@
 #include "cobalt/loader/fetcher_factory.h"
 #include "cobalt/loader/loader.h"
 #include "cobalt/loader/text_decoder.h"
+#include "net/http/http_request_headers.h"
 #include "url/gurl.h"
 
 namespace cobalt {
@@ -48,6 +49,14 @@
       const TextDecoder::TextAvailableCallback& script_available_callback,
       const Loader::OnCompleteFunction& load_complete_callback);
 
+  std::unique_ptr<Loader> CreateScriptLoader(
+      const GURL& url, const Origin& origin,
+      const csp::SecurityCallback& url_security_callback,
+      const TextDecoder::TextAvailableCallback& script_available_callback,
+      const TextDecoder::ResponseStartedCallback& response_started_callback,
+      const Loader::OnCompleteFunction& load_complete_callback,
+      net::HttpRequestHeaders headers = net::HttpRequestHeaders());
+
  protected:
   void OnLoaderCreated(Loader* loader);
   void OnLoaderDestroyed(Loader* loader);
@@ -55,7 +64,8 @@
   Loader::FetcherCreator MakeFetcherCreator(
       const GURL& url, const csp::SecurityCallback& url_security_callback,
       RequestMode request_mode, const Origin& origin,
-      disk_cache::ResourceType type = disk_cache::kOther);
+      disk_cache::ResourceType type = disk_cache::kOther,
+      net::HttpRequestHeaders headers = net::HttpRequestHeaders());
 
   // Ensures that the LoaderFactory methods are only called from the same
   // thread.
diff --git a/cobalt/loader/text_decoder.h b/cobalt/loader/text_decoder.h
index 07a2492..16ad4ce 100644
--- a/cobalt/loader/text_decoder.h
+++ b/cobalt/loader/text_decoder.h
@@ -34,6 +34,9 @@
 // results.
 class TextDecoder : public Decoder {
  public:
+  typedef base::Callback<bool(
+      Fetcher* fetcher, const scoped_refptr<net::HttpResponseHeaders>& headers)>
+      ResponseStartedCallback;
   typedef base::Callback<void(const loader::Origin&,
                               std::unique_ptr<std::string>)>
       TextAvailableCallback;
@@ -43,10 +46,24 @@
   // This function is used for binding callback for creating TextDecoder.
   static std::unique_ptr<Decoder> Create(
       const TextAvailableCallback& text_available_callback,
+      const ResponseStartedCallback& response_started_callback =
+          ResponseStartedCallback(),
       const loader::Decoder::OnCompleteFunction& load_complete_callback =
           loader::Decoder::OnCompleteFunction()) {
-    return std::unique_ptr<Decoder>(
-        new TextDecoder(text_available_callback, load_complete_callback));
+    return std::unique_ptr<Decoder>(new TextDecoder(text_available_callback,
+                                                    response_started_callback,
+                                                    load_complete_callback));
+  }
+
+  LoadResponseType OnResponseStarted(
+      Fetcher* fetcher, const scoped_refptr<net::HttpResponseHeaders>& headers)
+      override WARN_UNUSED_RESULT {
+    if (fetcher && headers && !response_started_callback_.is_null()) {
+      if (!response_started_callback_.Run(fetcher, headers)) {
+        return kLoadResponseAbort;
+      }
+    }
+    return kLoadResponseContinue;
   }
 
   // From Decoder.
@@ -105,13 +122,16 @@
  private:
   explicit TextDecoder(
       const TextAvailableCallback& text_available_callback,
+      const ResponseStartedCallback& response_started_callback,
       const loader::Decoder::OnCompleteFunction& load_complete_callback)
       : text_available_callback_(text_available_callback),
+        response_started_callback_(response_started_callback),
         load_complete_callback_(load_complete_callback),
         suspended_(false) {}
 
   THREAD_CHECKER(thread_checker_);
   TextAvailableCallback text_available_callback_;
+  ResponseStartedCallback response_started_callback_;
   loader::Decoder::OnCompleteFunction load_complete_callback_;
   loader::Origin last_url_origin_;
   std::unique_ptr<std::string> text_;
diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc
index 9a8fb5b..be1ea63 100644
--- a/cobalt/media/base/sbplayer_pipeline.cc
+++ b/cobalt/media/base/sbplayer_pipeline.cc
@@ -63,9 +63,9 @@
 using ::media::Demuxer;
 using ::media::DemuxerHost;
 using ::media::DemuxerStream;
-using ::media::VideoDecoderConfig;
 using ::media::PipelineStatistics;
 using ::media::PipelineStatusCallback;
+using ::media::VideoDecoderConfig;
 
 static const int kRetryDelayAtSuspendInMilliseconds = 100;
 
@@ -206,7 +206,7 @@
   // playback resume.
   std::string GetTimeInformation() const;
 
-  void RunSetDrmSystemReadyCB();
+  void RunSetDrmSystemReadyCB(DrmSystemReadyCB drm_system_ready_cb);
 
   // An identifier string for the pipeline, used in CVal to identify multiple
   // pipelines.
@@ -518,7 +518,7 @@
                  on_encrypted_media_init_data_encountered_cb);
   set_drm_system_ready_cb_ = parameters.set_drm_system_ready_cb;
   DCHECK(!set_drm_system_ready_cb_.is_null());
-  RunSetDrmSystemReadyCB();
+  RunSetDrmSystemReadyCB(base::Bind(&SbPlayerPipeline::SetDrmSystem, this));
 
   task_runner_->PostTask(FROM_HERE,
                          base::Bind(&SbPlayerPipeline::StartTask, this,
@@ -1130,7 +1130,8 @@
       content_size_change_cb_.Run();
     }
     if (is_encrypted) {
-      RunSetDrmSystemReadyCB();
+      RunSetDrmSystemReadyCB(::media::BindToCurrentLoop(
+          base::Bind(&SbPlayerPipeline::CreatePlayer, this)));
       return;
     }
   }
@@ -1537,16 +1538,11 @@
          ", time since last resume: " + time_since_resume;
 }
 
-void SbPlayerPipeline::RunSetDrmSystemReadyCB() {
+void SbPlayerPipeline::RunSetDrmSystemReadyCB(
+    DrmSystemReadyCB drm_system_ready_cb) {
   TRACE_EVENT0("cobalt::media", "SbPlayerPipeline::RunSetDrmSystemReadyCB");
   set_drm_system_ready_cb_time_ = SbTimeGetMonotonicNow();
-#if SB_HAS(PLAYER_WITH_URL)
-  set_drm_system_ready_cb_.Run(
-      base::Bind(&SbPlayerPipeline::SetDrmSystem, this));
-#else   // SB_HAS(PLAYER_WITH_URL)
-  set_drm_system_ready_cb_.Run(::media::BindToCurrentLoop(
-      base::Bind(&SbPlayerPipeline::CreatePlayer, this)));
-#endif  // SB_HAS(PLAYER_WITH_URL)
+  set_drm_system_ready_cb_.Run(drm_system_ready_cb);
 }
 
 }  // namespace
diff --git a/cobalt/persistent_storage/persistent_settings.cc b/cobalt/persistent_storage/persistent_settings.cc
index ffebc2a..822a5d3 100644
--- a/cobalt/persistent_storage/persistent_settings.cc
+++ b/cobalt/persistent_storage/persistent_settings.cc
@@ -17,7 +17,9 @@
 #include <utility>
 #include <vector>
 
-#include "base/values.h"
+#include "base/bind.h"
+#include "components/prefs/json_pref_store.h"
+#include "components/prefs/json_read_only_pref_store.h"
 #include "starboard/common/file.h"
 #include "starboard/common/log.h"
 #include "starboard/configuration_constants.h"
@@ -31,140 +33,253 @@
 // settings file has been validated.
 const char kValidated[] = "validated";
 
+// Signals the given WaitableEvent.
+void SignalWaitableEvent(base::WaitableEvent* event) { event->Signal(); }
+
 }  // namespace
 
-PersistentSettings::PersistentSettings(
-    const std::string& file_name,
-    scoped_refptr<base::SingleThreadTaskRunner> task_runner) {
+void PersistentSettings::WillDestroyCurrentMessageLoop() {
+  // Clear all member variables allocated from the thread.
+  pref_store_.reset();
+}
+
+PersistentSettings::PersistentSettings(const std::string& file_name)
+    : thread_("PersistentSettings") {
+  if (!thread_.Start()) return;
+  DCHECK(message_loop());
+
   std::vector<char> storage_dir(kSbFileMaxPath + 1, 0);
   SbSystemGetPath(kSbSystemPathStorageDirectory, storage_dir.data(),
                   kSbFileMaxPath);
 
   persistent_settings_file_ =
       std::string(storage_dir.data()) + kSbFileSepString + file_name;
-  SB_LOG(INFO) << "Persistent settings file path: "
-               << persistent_settings_file_;
+  LOG(INFO) << "Persistent settings file path: " << persistent_settings_file_;
 
-  task_runner_ = task_runner;
+  // Initialize pref_store_ with a JSONReadOnlyPrefStore, Used for
+  // synchronous PersistentSettings::Get calls made before the asynchronous
+  // InitializeWriteablePrefStore initializes pref_store_ with a writable
+  // instance.
+  {
+    base::AutoLock auto_lock(pref_store_lock_);
+    pref_store_ = base::MakeRefCounted<JsonReadOnlyPrefStore>(
+        base::FilePath(persistent_settings_file_));
+    pref_store_->ReadPrefs();
+  }
 
-  pref_store_ = base::MakeRefCounted<JsonPrefStore>(
-      base::FilePath(persistent_settings_file_));
-  pref_store_->ReadPrefs();
+  message_loop()->task_runner()->PostTask(
+      FROM_HERE, base::Bind(&PersistentSettings::InitializeWriteablePrefStore,
+                            base::Unretained(this)));
 
+  // Register as a destruction observer to shut down the Web Agent once all
+  // pending tasks have been executed and the message loop is about to be
+  // destroyed. This allows us to safely stop the thread, drain the task queue,
+  // then destroy the internal components before the message loop is reset.
+  // No posted tasks will be executed once the thread is stopped.
+  message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::Bind(&base::MessageLoop::AddDestructionObserver,
+                 base::Unretained(message_loop()), base::Unretained(this)));
+
+  // This works almost like a PostBlockingTask, except that any blocking that
+  // may be necessary happens when Stop() is called instead of right now.
+  message_loop()->task_runner()->PostTask(
+      FROM_HERE, base::Bind(&SignalWaitableEvent,
+                            base::Unretained(&destruction_observer_added_)));
+}
+
+PersistentSettings::~PersistentSettings() {
+  DCHECK(message_loop());
+  DCHECK(thread_.IsRunning());
+
+  // Ensure that the destruction observer got added and the pref store was
+  // initialized before stopping the thread. Stop the thread. This will cause
+  // the destruction observer to be notified.
+  writeable_pref_store_initialized_.Wait();
+  destruction_observer_added_.Wait();
+  thread_.Stop();
+}
+
+void PersistentSettings::InitializeWriteablePrefStore() {
+  DCHECK_EQ(base::MessageLoop::current(), message_loop());
+  {
+    base::AutoLock auto_lock(pref_store_lock_);
+    pref_store_ = base::MakeRefCounted<JsonPrefStore>(
+        base::FilePath(persistent_settings_file_));
+    pref_store_->ReadPrefs();
+    // PersistentSettings Set and Remove Helper methods will now be able to
+    // access the pref_store_ initialized from the dedicated thread_.
+    writeable_pref_store_initialized_.Signal();
+  }
   validated_initial_settings_ = GetPersistentSettingAsBool(kValidated, false);
-  if (!validated_initial_settings_)
+  if (!validated_initial_settings_) {
     starboard::SbFileDeleteRecursive(persistent_settings_file_.c_str(), true);
+  }
 }
 
 void PersistentSettings::ValidatePersistentSettings() {
-  task_runner_->PostTask(
+  message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(&PersistentSettings::ValidatePersistentSettingsHelper,
                      base::Unretained(this)));
 }
 
 void PersistentSettings::ValidatePersistentSettingsHelper() {
+  DCHECK_EQ(base::MessageLoop::current(), message_loop());
   if (!validated_initial_settings_) {
-    pref_store_->SetValue(kValidated, std::make_unique<base::Value>(true),
-                          WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
-    pref_store_->CommitPendingWrite();
+    base::AutoLock auto_lock(pref_store_lock_);
+    writeable_pref_store()->SetValue(
+        kValidated, std::make_unique<base::Value>(true),
+        WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
+    writeable_pref_store()->CommitPendingWrite();
     validated_initial_settings_ = true;
   }
 }
 
 bool PersistentSettings::GetPersistentSettingAsBool(const std::string& key,
                                                     bool default_setting) {
+  base::AutoLock auto_lock(pref_store_lock_);
   auto persistent_settings = pref_store_->GetValues();
   const base::Value* result = persistent_settings->FindKey(key);
   if (result && result->is_bool()) return result->GetBool();
-  SB_LOG(INFO) << "Persistent setting does not exist: " << key;
+  LOG(INFO) << "Persistent setting does not exist: " << key;
   return default_setting;
 }
 
 int PersistentSettings::GetPersistentSettingAsInt(const std::string& key,
                                                   int default_setting) {
+  base::AutoLock auto_lock(pref_store_lock_);
   auto persistent_settings = pref_store_->GetValues();
   const base::Value* result = persistent_settings->FindKey(key);
   if (result && result->is_int()) return result->GetInt();
-  SB_LOG(INFO) << "Persistent setting does not exist: " << key;
+  LOG(INFO) << "Persistent setting does not exist: " << key;
   return default_setting;
 }
 
 double PersistentSettings::GetPersistentSettingAsDouble(
     const std::string& key, double default_setting) {
+  base::AutoLock auto_lock(pref_store_lock_);
   auto persistent_settings = pref_store_->GetValues();
   const base::Value* result = persistent_settings->FindKey(key);
   if (result && result->is_double()) return result->GetDouble();
-  SB_LOG(INFO) << "Persistent setting does not exist: " << key;
+  LOG(INFO) << "Persistent setting does not exist: " << key;
   return default_setting;
 }
 
 std::string PersistentSettings::GetPersistentSettingAsString(
     const std::string& key, const std::string& default_setting) {
+  base::AutoLock auto_lock(pref_store_lock_);
   auto persistent_settings = pref_store_->GetValues();
   const base::Value* result = persistent_settings->FindKey(key);
   if (result && result->is_string()) return result->GetString();
-  SB_LOG(INFO) << "Persistent setting does not exist: " << key;
+  LOG(INFO) << "Persistent setting does not exist: " << key;
   return default_setting;
 }
 
+std::vector<base::Value> PersistentSettings::GetPersistentSettingAsList(
+    const std::string& key) {
+  base::AutoLock auto_lock(pref_store_lock_);
+  auto persistent_settings = pref_store_->GetValues();
+  base::Value* result = persistent_settings->FindKey(key);
+  if (result && result->is_list()) {
+    return std::move(result->TakeList());
+  }
+  LOG(INFO) << "Persistent setting does not exist: " << key;
+  return std::vector<base::Value>();
+}
+
+base::flat_map<std::string, std::unique_ptr<base::Value>>
+PersistentSettings::GetPersistentSettingAsDictionary(const std::string& key) {
+  base::AutoLock auto_lock(pref_store_lock_);
+  auto persistent_settings = pref_store_->GetValues();
+  base::Value* result = persistent_settings->FindKey(key);
+  base::flat_map<std::string, std::unique_ptr<base::Value>> dict;
+  if (result && result->is_dict()) {
+    for (const auto& key_value : result->DictItems()) {
+      dict.insert(std::make_pair(
+          key_value.first,
+          std::make_unique<base::Value>(std::move(key_value.second))));
+    }
+    return dict;
+  }
+  LOG(INFO) << "Persistent setting does not exist: " << key;
+  return dict;
+}
+
 void PersistentSettings::SetPersistentSetting(
-    const std::string& key, std::unique_ptr<base::Value> value) {
+    const std::string& key, std::unique_ptr<base::Value> value,
+    base::OnceClosure closure) {
   if (key == kValidated) {
-    SB_LOG(ERROR) << "Cannot set protected persistent setting: " << key;
+    LOG(ERROR) << "Cannot set protected persistent setting: " << key;
     return;
   }
-  task_runner_->PostTask(
+  message_loop()->task_runner()->PostTask(
       FROM_HERE, base::BindOnce(&PersistentSettings::SetPersistentSettingHelper,
-                                base::Unretained(this), key, std::move(value)));
+                                base::Unretained(this), key, std::move(value),
+                                std::move(closure)));
 }
 
 void PersistentSettings::SetPersistentSettingHelper(
-    const std::string& key, std::unique_ptr<base::Value> value) {
+    const std::string& key, std::unique_ptr<base::Value> value,
+    base::OnceClosure closure) {
+  DCHECK_EQ(base::MessageLoop::current(), message_loop());
   if (validated_initial_settings_) {
-    pref_store_->SetValue(kValidated, std::make_unique<base::Value>(false),
-                          WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
-    pref_store_->SetValue(key, std::move(value),
-                          WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
-    pref_store_->CommitPendingWrite();
+    base::AutoLock auto_lock(pref_store_lock_);
+    writeable_pref_store()->SetValue(
+        kValidated, std::make_unique<base::Value>(false),
+        WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
+    writeable_pref_store()->SetValue(
+        key, std::move(value), WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
+    writeable_pref_store()->CommitPendingWrite();
   } else {
-    SB_LOG(ERROR) << "Cannot set persistent setting while unvalidated: " << key;
+    LOG(ERROR) << "Cannot set persistent setting while unvalidated: " << key;
   }
+  std::move(closure).Run();
 }
 
-void PersistentSettings::RemovePersistentSetting(const std::string& key) {
-  task_runner_->PostTask(
+void PersistentSettings::RemovePersistentSetting(const std::string& key,
+                                                 base::OnceClosure closure) {
+  message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(&PersistentSettings::RemovePersistentSettingHelper,
-                     base::Unretained(this), key));
+                     base::Unretained(this), key, std::move(closure)));
 }
 
-void PersistentSettings::RemovePersistentSettingHelper(const std::string& key) {
+void PersistentSettings::RemovePersistentSettingHelper(
+    const std::string& key, base::OnceClosure closure) {
+  DCHECK_EQ(base::MessageLoop::current(), message_loop());
   if (validated_initial_settings_) {
-    pref_store_->SetValue(kValidated, std::make_unique<base::Value>(false),
-                          WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
-    pref_store_->RemoveValue(key, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
-    pref_store_->CommitPendingWrite();
+    base::AutoLock auto_lock(pref_store_lock_);
+    writeable_pref_store()->SetValue(
+        kValidated, std::make_unique<base::Value>(false),
+        WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
+    writeable_pref_store()->RemoveValue(
+        key, WriteablePrefStore::DEFAULT_PREF_WRITE_FLAGS);
+    writeable_pref_store()->CommitPendingWrite();
   } else {
-    SB_LOG(ERROR) << "Cannot remove persistent setting while unvalidated: "
-                  << key;
+    LOG(ERROR) << "Cannot remove persistent setting while unvalidated: " << key;
   }
+  std::move(closure).Run();
 }
 
-void PersistentSettings::DeletePersistentSettings() {
-  task_runner_->PostTask(
+void PersistentSettings::DeletePersistentSettings(base::OnceClosure closure) {
+  message_loop()->task_runner()->PostTask(
       FROM_HERE,
       base::BindOnce(&PersistentSettings::DeletePersistentSettingsHelper,
-                     base::Unretained(this)));
+                     base::Unretained(this), std::move(closure)));
 }
 
-void PersistentSettings::DeletePersistentSettingsHelper() {
+void PersistentSettings::DeletePersistentSettingsHelper(
+    base::OnceClosure closure) {
+  DCHECK_EQ(base::MessageLoop::current(), message_loop());
   if (validated_initial_settings_) {
     starboard::SbFileDeleteRecursive(persistent_settings_file_.c_str(), true);
-    pref_store_->ReadPrefs();
+    base::AutoLock auto_lock(pref_store_lock_);
+    writeable_pref_store()->ReadPrefs();
   } else {
-    SB_LOG(ERROR) << "Cannot delete persistent setting while unvalidated.";
+    LOG(ERROR) << "Cannot delete persistent setting while unvalidated.";
   }
+  std::move(closure).Run();
 }
 
 }  // namespace persistent_storage
diff --git a/cobalt/persistent_storage/persistent_settings.h b/cobalt/persistent_storage/persistent_settings.h
index eaadd7b..47cd446 100644
--- a/cobalt/persistent_storage/persistent_settings.h
+++ b/cobalt/persistent_storage/persistent_settings.h
@@ -17,8 +17,18 @@
 
 #include <memory>
 #include <string>
+#include <utility>
+#include <vector>
 
-#include "components/prefs/json_pref_store.h"
+#include "base/bind_helpers.h"
+#include "base/callback_forward.h"
+#include "base/containers/flat_map.h"
+#include "base/message_loop/message_loop.h"
+#include "base/synchronization/lock.h"
+#include "base/synchronization/waitable_event.h"
+#include "base/threading/thread.h"
+#include "base/values.h"
+#include "components/prefs/persistent_pref_store.h"
 
 namespace cobalt {
 namespace persistent_storage {
@@ -27,51 +37,97 @@
 // lifecycle to another as a JSON file in kSbSystemPathStorageDirectory.
 // PersistentSettings uses JsonPrefStore for most of its functionality.
 // JsonPrefStore maintains thread safety by requiring that all access occurs on
-// the Sequence that created it. This is why some functions rely on a PostTask
-// to the SingleThreadTaskRunner that calls and is subsequently passed into
-// PersistentSettings' constructor.
-class PersistentSettings {
+// the Sequence that created it.
+class PersistentSettings : public base::MessageLoop::DestructionObserver {
  public:
-  explicit PersistentSettings(
-      const std::string& file_name,
-      scoped_refptr<base::SingleThreadTaskRunner> task_runner);
+  explicit PersistentSettings(const std::string& file_name);
+  ~PersistentSettings();
+
+  // The message loop this object is running on.
+  base::MessageLoop* message_loop() const { return thread_.message_loop(); }
+
+  // From base::MessageLoop::DestructionObserver.
+  void WillDestroyCurrentMessageLoop() override;
 
   // Validates persistent settings by restoring the file on successful start up.
   void ValidatePersistentSettings();
 
   // Getters and Setters for persistent settings.
   bool GetPersistentSettingAsBool(const std::string& key, bool default_setting);
+
   int GetPersistentSettingAsInt(const std::string& key, int default_setting);
+
   double GetPersistentSettingAsDouble(const std::string& key,
                                       double default_setting);
+
   std::string GetPersistentSettingAsString(const std::string& key,
                                            const std::string& default_setting);
-  void SetPersistentSetting(const std::string& key,
-                            std::unique_ptr<base::Value> value);
-  void RemovePersistentSetting(const std::string& key);
 
-  void DeletePersistentSettings();
+  std::vector<base::Value> GetPersistentSettingAsList(const std::string& key);
+
+  base::flat_map<std::string, std::unique_ptr<base::Value>>
+  GetPersistentSettingAsDictionary(const std::string& key);
+
+  void SetPersistentSetting(
+      const std::string& key, std::unique_ptr<base::Value> value,
+      base::OnceClosure closure = std::move(base::DoNothing()));
+
+  void RemovePersistentSetting(
+      const std::string& key,
+      base::OnceClosure closure = std::move(base::DoNothing()));
+
+  void DeletePersistentSettings(
+      base::OnceClosure closure = std::move(base::DoNothing()));
 
  private:
-  // Persistent settings file path.
-  std::string persistent_settings_file_;
-  // TaskRunner of the thread that initialized PersistentSettings and
-  // PrefStore.
-  scoped_refptr<base::SingleThreadTaskRunner> task_runner_;
-  // PrefStore used for persistent settings.
-  scoped_refptr<JsonPrefStore> pref_store_;
-  // Flag indicating whether or not initial persistent settings have been
-  // validated.
-  bool validated_initial_settings_;
+  // Called by the constructor to initialize pref_store_ from
+  // the dedicated thread_ as a writeable JSONPrefStore.
+  void InitializeWriteablePrefStore();
 
   void ValidatePersistentSettingsHelper();
 
   void SetPersistentSettingHelper(const std::string& key,
-                                  std::unique_ptr<base::Value> value);
+                                  std::unique_ptr<base::Value> value,
+                                  base::OnceClosure closure);
 
-  void RemovePersistentSettingHelper(const std::string& key);
+  void RemovePersistentSettingHelper(const std::string& key,
+                                     base::OnceClosure closure);
 
-  void DeletePersistentSettingsHelper();
+  void DeletePersistentSettingsHelper(base::OnceClosure closure);
+
+  scoped_refptr<PersistentPrefStore> writeable_pref_store() {
+    writeable_pref_store_initialized_.Wait();
+    return pref_store_;
+  }
+
+  // Persistent settings file path.
+  std::string persistent_settings_file_;
+
+  // PrefStore used for persistent settings.
+  scoped_refptr<PersistentPrefStore> pref_store_;
+
+  // Flag indicating whether or not initial persistent settings have been
+  // validated.
+  bool validated_initial_settings_;
+
+  // The thread created and owned by PersistentSettings. All pref_store_
+  // methods must be called from this thread.
+  base::Thread thread_;
+
+  // This event is used to signal when Initialize has been called and
+  // pref_store_ mutations can now occur.
+  base::WaitableEvent writeable_pref_store_initialized_ = {
+      base::WaitableEvent::ResetPolicy::MANUAL,
+      base::WaitableEvent::InitialState::NOT_SIGNALED};
+
+  // This event is used to signal when the destruction observers have been
+  // added to the message loop. This is then used in Stop() to ensure that
+  // processing doesn't continue until the thread is cleanly shutdown.
+  base::WaitableEvent destruction_observer_added_ = {
+      base::WaitableEvent::ResetPolicy::MANUAL,
+      base::WaitableEvent::InitialState::NOT_SIGNALED};
+
+  base::Lock pref_store_lock_;
 };
 
 }  // namespace persistent_storage
diff --git a/cobalt/persistent_storage/persistent_settings_test.cc b/cobalt/persistent_storage/persistent_settings_test.cc
index a4632e9..4a3f3a7 100644
--- a/cobalt/persistent_storage/persistent_settings_test.cc
+++ b/cobalt/persistent_storage/persistent_settings_test.cc
@@ -12,9 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+#include <utility>
 #include <vector>
 
+#include "base/bind.h"
+#include "base/bind_internal.h"
+#include "base/callback_forward.h"
+#include "base/synchronization/waitable_event.h"
 #include "base/test/scoped_task_environment.h"
+#include "base/threading/platform_thread.h"
 #include "base/values.h"
 #include "cobalt/persistent_storage/persistent_settings.h"
 #include "starboard/common/file.h"
@@ -43,26 +49,27 @@
 
     persistent_settings_file_ = std::string(storage_dir.data()) +
                                 kSbFileSepString + kPersistentSettingsJson;
-    SB_LOG(INFO) << "Persistent settings test file path: "
-                 << persistent_settings_file_;
   }
 
   void SetUp() final {
+    test_done_.Reset();
     starboard::SbFileDeleteRecursive(persistent_settings_file_.c_str(), true);
   }
 
   void TearDown() final {
+    test_done_.Reset();
     starboard::SbFileDeleteRecursive(persistent_settings_file_.c_str(), true);
   }
 
   base::test::ScopedTaskEnvironment scoped_task_environment_;
   std::string persistent_settings_file_;
+  base::WaitableEvent test_done_ = {
+      base::WaitableEvent::ResetPolicy::MANUAL,
+      base::WaitableEvent::InitialState::NOT_SIGNALED};
 };
 
 TEST_F(PersistentSettingTest, GetDefaultBool) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   // does not exist
@@ -70,35 +77,60 @@
   ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
 
   // exists but invalid
-  persistent_settings->SetPersistentSetting("key",
-                                            std::make_unique<base::Value>(4.2));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsInt("key", true));
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsInt("key", true));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(4.2), std::move(closure));
+  test_done_.Wait();
+  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, GetSetBool) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
-  persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(false));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
+      "key", std::make_unique<base::Value>(true), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_FALSE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_FALSE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(false), std::move(closure));
+
+  test_done_.Wait();
+  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, GetDefaultInt) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   // does not exist
@@ -107,38 +139,61 @@
   ASSERT_EQ(42, persistent_settings->GetPersistentSettingAsInt("key", 42));
 
   // exists but invalid
-  persistent_settings->SetPersistentSetting("key",
-                                            std::make_unique<base::Value>(4.2));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ(8, persistent_settings->GetPersistentSettingAsInt("key", 8));
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ(8, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(4.2), std::move(closure));
+  test_done_.Wait();
 }
 
 TEST_F(PersistentSettingTest, GetSetInt) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
-  persistent_settings->SetPersistentSetting("key",
-                                            std::make_unique<base::Value>(-1));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ(-1, persistent_settings->GetPersistentSettingAsInt("key", 8));
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ(-1, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(-1), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 
-  persistent_settings->SetPersistentSetting("key",
-                                            std::make_unique<base::Value>(0));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ(0, persistent_settings->GetPersistentSettingAsInt("key", 8));
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ(0, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(0), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 
-  persistent_settings->SetPersistentSetting("key",
-                                            std::make_unique<base::Value>(42));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ(42, persistent_settings->GetPersistentSettingAsInt("key", 8));
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ(42, persistent_settings->GetPersistentSettingAsInt("key", 8));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(42), std::move(closure));
+  test_done_.Wait();
 }
 
 TEST_F(PersistentSettingTest, GetDefaultString) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   // does not exist
@@ -153,127 +208,225 @@
             persistent_settings->GetPersistentSettingAsString("key", "\\n"));
 
   // exists but invalid
-  persistent_settings->SetPersistentSetting("key",
-                                            std::make_unique<base::Value>(4.2));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ("hello",
-            persistent_settings->GetPersistentSettingAsString("key", "hello"));
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ("hello", persistent_settings->GetPersistentSettingAsString(
+                               "key", "hello"));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>(4.2), std::move(closure));
+  test_done_.Wait();
+  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, GetSetString) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
-  persistent_settings->SetPersistentSetting("key",
-                                            std::make_unique<base::Value>(""));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ("",
-            persistent_settings->GetPersistentSettingAsString("key", "hello"));
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ("", persistent_settings->GetPersistentSettingAsString(
+                          "key", "hello"));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("hello there"));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ("hello there",
+      "key", std::make_unique<base::Value>(""), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ(
+            "hello there",
             persistent_settings->GetPersistentSettingAsString("key", "hello"));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
 
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("42"));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ("42",
-            persistent_settings->GetPersistentSettingAsString("key", "hello"));
+      "key", std::make_unique<base::Value>("hello there"), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ("42", persistent_settings->GetPersistentSettingAsString(
+                            "key", "hello"));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("\n"));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ("\n",
-            persistent_settings->GetPersistentSettingAsString("key", "hello"));
+      "key", std::make_unique<base::Value>("42"), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ("\n", persistent_settings->GetPersistentSettingAsString(
+                            "key", "hello"));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>("\\n"));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_EQ("\\n",
-            persistent_settings->GetPersistentSettingAsString("key", "hello"));
+      "key", std::make_unique<base::Value>("\n"), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_EQ("\\n", persistent_settings->GetPersistentSettingAsString(
+                             "key", "hello"));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->SetPersistentSetting(
+      "key", std::make_unique<base::Value>("\\n"), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 }
 
 TEST_F(PersistentSettingTest, RemoveSetting) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
   ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
 
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
+      "key", std::make_unique<base::Value>(true), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 
-  persistent_settings->RemovePersistentSetting("key");
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_FALSE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->RemovePersistentSetting("key", std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, DeleteSettings) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
   ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
 
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
+      "key", std::make_unique<base::Value>(true), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 
-  persistent_settings->DeletePersistentSettings();
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_FALSE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
+  persistent_settings->DeletePersistentSettings(std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  delete persistent_settings;
 }
 
 TEST_F(PersistentSettingTest, InvalidSettings) {
-  auto persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  auto persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
   ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
 
+  base::OnceClosure closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(true));
-  scoped_task_environment_.RunUntilIdle();
+      "key", std::make_unique<base::Value>(true), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
+
+  delete persistent_settings;
+  // Sleep for one second to allow for the previous persistent_setting's
+  // JsonPrefStore instance time to write to disk before creating a new
+  // persistent_settings and JsonPrefStore instance.
+  base::PlatformThread::Sleep(base::TimeDelta::FromSeconds(1));
+  persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
 
-  persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
-
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
-
+  closure = base::BindOnce(
+      [](PersistentSettings* persistent_settings,
+         base::WaitableEvent* test_done) {
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", true));
+        ASSERT_TRUE(
+            persistent_settings->GetPersistentSettingAsBool("key", false));
+        test_done->Signal();
+      },
+      persistent_settings, &test_done_);
   persistent_settings->SetPersistentSetting(
-      "key", std::make_unique<base::Value>(false));
-  scoped_task_environment_.RunUntilIdle();
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
-  ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", false));
+      "key", std::make_unique<base::Value>(false), std::move(closure));
+  test_done_.Wait();
+  test_done_.Reset();
 
-  persistent_settings = std::make_unique<PersistentSettings>(
-      kPersistentSettingsJson,
-      scoped_task_environment_.GetMainThreadTaskRunner());
+  delete persistent_settings;
+  persistent_settings = new PersistentSettings(kPersistentSettingsJson);
   persistent_settings->ValidatePersistentSettings();
 
   ASSERT_TRUE(persistent_settings->GetPersistentSettingAsBool("key", true));
   ASSERT_FALSE(persistent_settings->GetPersistentSettingAsBool("key", false));
+
+  delete persistent_settings;
 }
 
 }  // namespace persistent_storage
diff --git a/cobalt/script/fake_global_environment.h b/cobalt/script/fake_global_environment.h
index 51760fa..c7376b8 100644
--- a/cobalt/script/fake_global_environment.h
+++ b/cobalt/script/fake_global_environment.h
@@ -28,6 +28,10 @@
   void CreateGlobalObject() override {}
   Wrappable* global_wrappable() const override { return nullptr; };
 
+  v8::MaybeLocal<v8::Script> Compile(
+      const scoped_refptr<SourceCode>& source_code) {
+    return {};
+  }
   bool EvaluateScript(const scoped_refptr<SourceCode>& script_utf8,
                       std::string* out_result) override {
     return false;
@@ -51,6 +55,7 @@
   void DisableEval(const std::string& message) override {}
   void EnableEval() override {}
   void DisableJit() override {}
+  bool IsEvalEnabled() override { return true; }
   void SetReportEvalCallback(const base::Closure& report_eval) override {}
   void SetReportErrorCallback(const ReportErrorCallback& report_eval) override {
   }
diff --git a/cobalt/script/global_environment.h b/cobalt/script/global_environment.h
index 4e569f3..3b6c506 100644
--- a/cobalt/script/global_environment.h
+++ b/cobalt/script/global_environment.h
@@ -60,6 +60,10 @@
   // Returns the global object if it is a wrappable.
   virtual Wrappable* global_wrappable() const = 0;
 
+  // Compiles the code but does not execute.
+  virtual v8::MaybeLocal<v8::Script> Compile(
+      const scoped_refptr<SourceCode>& source_code) = 0;
+
   // Evaluate the JavaScript source code. Returns true on success,
   // false if there is an exception.
   // If out_result is non-NULL, it will be set to hold the result of the script
@@ -100,6 +104,8 @@
   // Allow eval().
   virtual void EnableEval() = 0;
 
+  virtual bool IsEvalEnabled() = 0;
+
   // Disable just-in-time compilation of JavaScript source code to native
   // code.  Calling this is a no-op if JIT was not enabled in the first place,
   // or if the engine does not support disabling JIT.
diff --git a/cobalt/script/promise.h b/cobalt/script/promise.h
index d9e1c17..4500466 100644
--- a/cobalt/script/promise.h
+++ b/cobalt/script/promise.h
@@ -22,6 +22,7 @@
 #include "cobalt/script/exception_message.h"
 #include "cobalt/script/script_exception.h"
 #include "cobalt/script/script_value.h"
+#include "v8/include/v8.h"
 
 namespace cobalt {
 namespace script {
@@ -59,6 +60,9 @@
   virtual void AddStateChangeCallback(
       std::unique_ptr<base::OnceCallback<void()>> callback) = 0;
 
+  virtual v8::Local<v8::Promise> promise() const = 0;
+  virtual v8::Local<v8::Promise::Resolver> resolver() const = 0;
+
   virtual ~Promise() {}
 };
 
diff --git a/cobalt/script/script_value.h b/cobalt/script/script_value.h
index e1345e2..25567d7 100644
--- a/cobalt/script/script_value.h
+++ b/cobalt/script/script_value.h
@@ -17,6 +17,7 @@
 
 #include <algorithm>
 #include <memory>
+#include <utility>
 
 #include "base/basictypes.h"
 #include "base/logging.h"
@@ -171,8 +172,10 @@
       : Handle(reference.referenced_value().MakeWeakCopy().release()) {}
 
   Handle(const Handle& other) : script_value_(other.script_value_) {
-    script_value_->PreventGarbageCollection();
-    script_value_->reference_count_++;
+    if (script_value_) {
+      script_value_->PreventGarbageCollection();
+      script_value_->reference_count_++;
+    }
   }
   // We need the default constructor for nullable ScriptValue.
   Handle() = default;
diff --git a/cobalt/script/script_value_factory.h b/cobalt/script/script_value_factory.h
index 393f60b..c3abbf5 100644
--- a/cobalt/script/script_value_factory.h
+++ b/cobalt/script/script_value_factory.h
@@ -21,10 +21,16 @@
 #include "cobalt/script/array_buffer.h"
 #include "cobalt/script/promise.h"
 #include "cobalt/script/script_value.h"
+#include "cobalt/script/value_handle.h"
 
 namespace cobalt {
 namespace script {
 
+using Any = script::Handle<script::ValueHandle>;
+using PromiseAny = script::Promise<script::Handle<script::ValueHandle>>;
+using HandlePromiseAny = script::Handle<PromiseAny>;
+using ValuePromiseAny = ScriptValue<PromiseAny>;
+
 using SequenceWrappable = Sequence<scoped_refptr<Wrappable>>;
 using PromiseSequenceWrappable = Promise<SequenceWrappable>;
 using HandlePromiseSequenceWrappable = Handle<PromiseSequenceWrappable>;
diff --git a/cobalt/script/script_value_factory_instantiations.h b/cobalt/script/script_value_factory_instantiations.h
index ad37696..a7cc56d 100644
--- a/cobalt/script/script_value_factory_instantiations.h
+++ b/cobalt/script/script_value_factory_instantiations.h
@@ -49,6 +49,7 @@
 PROMISE_TEMPLATE_INSTANTIATION(scoped_refptr<Wrappable>);
 PROMISE_TEMPLATE_INSTANTIATION(Sequence<scoped_refptr<Wrappable>>);
 PROMISE_TEMPLATE_INSTANTIATION(BufferSource);
+PROMISE_TEMPLATE_INSTANTIATION(Handle<ValueHandle>);
 
 #undef PROMISE_TEMPLATE_INSTANTIATION
 
diff --git a/cobalt/script/source_code.h b/cobalt/script/source_code.h
index 7b51fe9..28773d7 100644
--- a/cobalt/script/source_code.h
+++ b/cobalt/script/source_code.h
@@ -33,6 +33,12 @@
       const std::string& script_utf8,
       const base::SourceLocation& script_location, bool is_muted = false);
 
+  // Same as |CreateSourceCode()| with a flag to indicate that the compiled
+  // JavaScript should not be cached.
+  static scoped_refptr<SourceCode> CreateSourceCodeWithoutCaching(
+      const std::string& script_utf8,
+      const base::SourceLocation& script_location, bool is_muted = false);
+
  protected:
   SourceCode() {}
   virtual ~SourceCode() {}
diff --git a/cobalt/script/v8c/conversion_helpers.h b/cobalt/script/v8c/conversion_helpers.h
index 81bf303..d1779af 100644
--- a/cobalt/script/v8c/conversion_helpers.h
+++ b/cobalt/script/v8c/conversion_helpers.h
@@ -85,7 +85,8 @@
 inline void ToJSValue(v8::Isolate* isolate, const std::string& in_string,
                       v8::Local<v8::Value>* out_value) {
   v8::MaybeLocal<v8::String> maybe_string = v8::String::NewFromUtf8(
-      isolate, in_string.data(), v8::NewStringType::kNormal, in_string.size());
+      isolate, in_string.data(), v8::NewStringType::kNormal,
+      static_cast<int>(in_string.size()));
   v8::Local<v8::Value> string;
   if (!maybe_string.ToLocal(&string)) {
     *out_value = v8::Null(isolate);
@@ -107,7 +108,8 @@
 inline void ToJSValue(v8::Isolate* isolate, const std::vector<uint8_t>& in_data,
                       v8::Local<v8::Value>* out_value) {
   v8::MaybeLocal<v8::String> maybe_string = v8::String::NewFromOneByte(
-      isolate, in_data.data(), v8::NewStringType::kNormal, in_data.size());
+      isolate, in_data.data(), v8::NewStringType::kNormal,
+      static_cast<int>(in_data.size()));
   v8::Local<v8::Value> string;
   if (!maybe_string.ToLocal(&string)) {
     *out_value = v8::Null(isolate);
@@ -165,12 +167,12 @@
 }
 
 template <typename T>
-inline const double UpperBound() {
+inline double UpperBound() {
   return std::numeric_limits<T>::max();
 }
 
 template <typename T>
-inline const double LowerBound() {
+inline double LowerBound() {
   return std::numeric_limits<T>::min();
 }
 
@@ -179,19 +181,19 @@
 // step 1 of ConvertToInt, see:
 // https://heycam.github.io/webidl/#abstract-opdef-converttoint
 template <>
-inline const double UpperBound<int64_t>() {
+inline double UpperBound<int64_t>() {
   const double kInt64UpperBound = static_cast<double>((1ll << 53) - 1);
   return kInt64UpperBound;
 }
 
 template <>
-inline const double LowerBound<int64_t>() {
+inline double LowerBound<int64_t>() {
   const double kInt64LowerBound = static_cast<double>(-(1ll << 53) + 1);
   return kInt64LowerBound;
 }
 
 template <>
-inline const double UpperBound<uint64_t>() {
+inline double UpperBound<uint64_t>() {
   const double kUInt64UpperBound = static_cast<double>((1ll << 53) - 1);
   return kUInt64UpperBound;
 }
@@ -602,8 +604,7 @@
 
     // 4.3. Let P be the result of calling ToString(i).
     // 4.4. Call CreateDataProperty(A, P, E).
-    v8::Maybe<bool> set_result =
-        array->Set(isolate->GetCurrentContext(), index, element);
+    array->Set(isolate->GetCurrentContext(), index, element).Check();
 
     // 4.5. Set i to i + 1.
   }
diff --git a/cobalt/script/v8c/helpers.h b/cobalt/script/v8c/helpers.h
index c2bfbef..2529039 100644
--- a/cobalt/script/v8c/helpers.h
+++ b/cobalt/script/v8c/helpers.h
@@ -15,6 +15,8 @@
 #ifndef COBALT_SCRIPT_V8C_HELPERS_H_
 #define COBALT_SCRIPT_V8C_HELPERS_H_
 
+#include <string>
+
 #include "starboard/common/string.h"
 #include "v8/include/v8.h"
 
@@ -35,7 +37,7 @@
   }
   return v8::String::NewFromOneByte(
              isolate, reinterpret_cast<const uint8_t*>(string),
-             v8::NewStringType::kInternalized, strlen(string))
+             v8::NewStringType::kInternalized, static_cast<int>(strlen(string)))
       .ToLocalChecked();
 }
 
diff --git a/cobalt/script/v8c/script_promise.h b/cobalt/script/v8c/script_promise.h
index 93c4561..7ae2c44 100644
--- a/cobalt/script/v8c/script_promise.h
+++ b/cobalt/script/v8c/script_promise.h
@@ -111,17 +111,17 @@
     }
   }
 
-  v8::Local<v8::Promise> promise() const {
+  v8::Local<v8::Promise> promise() const override {
     DCHECK(!this->IsEmpty());
     return resolver()->GetPromise();
   }
 
- protected:
-  v8::Local<v8::Promise::Resolver> resolver() const {
+  v8::Local<v8::Promise::Resolver> resolver() const override {
     DCHECK(!this->IsEmpty());
     return this->Get().Get(isolate_).template As<v8::Promise::Resolver>();
   }
 
+ protected:
   v8::Isolate* isolate() const { return isolate_; }
 
  private:
diff --git a/cobalt/script/v8c/v8c_array_buffer.h b/cobalt/script/v8c/v8c_array_buffer.h
index 17d468c..e1d1212 100644
--- a/cobalt/script/v8c/v8c_array_buffer.h
+++ b/cobalt/script/v8c/v8c_array_buffer.h
@@ -36,7 +36,7 @@
   V8cArrayBuffer() = default;
 
   V8cArrayBuffer(v8::Isolate* isolate, v8::Local<v8::Value> value)
-      : isolate_(isolate), ScopedPersistent(isolate, value) {
+      : ScopedPersistent(isolate, value), isolate_(isolate) {
     DCHECK(value->IsArrayBuffer());
   }
 
@@ -95,8 +95,7 @@
     return;
   }
 
-  *out_array_buffer =
-      std::move(V8cUserObjectHolder<V8cArrayBuffer>(isolate, value));
+  *out_array_buffer = V8cUserObjectHolder<V8cArrayBuffer>(isolate, value);
 }
 
 }  // namespace v8c
diff --git a/cobalt/script/v8c/v8c_array_buffer_view.h b/cobalt/script/v8c/v8c_array_buffer_view.h
index caaf6a2..0c00986 100644
--- a/cobalt/script/v8c/v8c_array_buffer_view.h
+++ b/cobalt/script/v8c/v8c_array_buffer_view.h
@@ -36,7 +36,7 @@
   // Default constructor should only be used by bindings code.
   V8cArrayBufferView() = default;
   V8cArrayBufferView(v8::Isolate* isolate, v8::Local<v8::Value> value)
-      : isolate_(isolate), ScopedPersistent(isolate, value) {
+      : ScopedPersistent(isolate, value), isolate_(isolate) {
     DCHECK(value->IsArrayBufferView());
   }
 
@@ -111,7 +111,7 @@
   }
 
   *out_array_buffer_view =
-      std::move(V8cUserObjectHolder<V8cArrayBufferView>(isolate, value));
+      V8cUserObjectHolder<V8cArrayBufferView>(isolate, value);
 }
 
 }  // namespace v8c
diff --git a/cobalt/script/v8c/v8c_data_view.h b/cobalt/script/v8c/v8c_data_view.h
index 0a0ae84..d8f56ee 100644
--- a/cobalt/script/v8c/v8c_data_view.h
+++ b/cobalt/script/v8c/v8c_data_view.h
@@ -35,7 +35,7 @@
   // Default constructor should only be used by bindings code.
   V8cDataView() = default;
   V8cDataView(v8::Isolate* isolate, v8::Local<v8::Value> value)
-      : isolate_(isolate), ScopedPersistent(isolate, value) {
+      : ScopedPersistent(isolate, value), isolate_(isolate) {
     DCHECK(value->IsDataView());
   }
 
@@ -110,8 +110,7 @@
     return;
   }
 
-  *out_array_buffer_view =
-      std::move(V8cUserObjectHolder<V8cDataView>(isolate, value));
+  *out_array_buffer_view = V8cUserObjectHolder<V8cDataView>(isolate, value);
 }
 
 }  // namespace v8c
diff --git a/cobalt/script/v8c/v8c_global_environment.cc b/cobalt/script/v8c/v8c_global_environment.cc
index 8c382db..0fd5937 100644
--- a/cobalt/script/v8c/v8c_global_environment.cc
+++ b/cobalt/script/v8c/v8c_global_environment.cc
@@ -279,6 +279,14 @@
   context->AllowCodeGenerationFromStrings(true);
 }
 
+bool V8cGlobalEnvironment::IsEvalEnabled() {
+  DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
+
+  EntryScope entry_scope(isolate_);
+  v8::Local<v8::Context> context = isolate_->GetCurrentContext();
+  return context->IsCodeGenerationFromStringsAllowed();
+}
+
 void V8cGlobalEnvironment::DisableJit() {
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
   LOG(INFO) << "V8 version " << V8_MAJOR_VERSION << '.' << V8_MINOR_VERSION
@@ -424,7 +432,7 @@
   // up by our caller.
 
   v8::Local<v8::Script> script;
-  if (!CompileWithCaching(source_code).ToLocal(&script)) {
+  if (!Compile(source_code).ToLocal(&script)) {
     return {};
   }
 
@@ -432,9 +440,9 @@
   return script->Run(isolate_->GetCurrentContext());
 }
 
-v8::MaybeLocal<v8::Script> V8cGlobalEnvironment::CompileWithCaching(
+v8::MaybeLocal<v8::Script> V8cGlobalEnvironment::Compile(
     const scoped_refptr<SourceCode>& source_code) {
-  TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::CompileWithCaching()");
+  TRACE_EVENT0("cobalt::script", "V8cGlobalEnvironment::Compile()");
   v8::Local<v8::Context> context = isolate_->GetCurrentContext();
   V8cSourceCode* v8c_source_code =
       base::polymorphic_downcast<V8cSourceCode*>(source_code.get());
@@ -471,6 +479,18 @@
       /*resource_is_shared_cross_origin=*/
       v8::Boolean::New(isolate_, v8c_source_code->is_muted()));
 
+  if (!v8c_source_code->should_cache_compiled_javascript()) {
+    v8::Local<v8::Script> script;
+    {
+      TRACE_EVENT0("cobalt::script", "v8::Script::Compile()");
+      if (!v8::Script::Compile(context, source, &script_origin)
+               .ToLocal(&script)) {
+        return {};
+      }
+    }
+    return script;
+  }
+
   std::string javascript_engine_version =
       script::GetJavaScriptEngineNameAndVersion();
   uint32_t javascript_cache_key = CreateJavaScriptCacheKey(
@@ -478,19 +498,22 @@
       v8c_source_code->source_utf8(), source_location.file_path);
   auto retrieved_cached_data = cobalt::cache::Cache::GetInstance()->Retrieve(
       disk_cache::ResourceType::kCompiledScript, javascript_cache_key,
-      [&]() -> std::unique_ptr<std::vector<uint8_t>> {
+      [&]() -> std::pair<std::unique_ptr<std::vector<uint8_t>>,
+                         base::Optional<base::Value>> {
         v8::Local<v8::Script> script;
         {
           TRACE_EVENT0("cobalt::script", "v8::Script::Compile()");
           if (!v8::Script::Compile(context, source, &script_origin)
                    .ToLocal(&script)) {
-            return nullptr;
+            return std::make_pair(/*data=*/nullptr, /*metadata=*/base::nullopt);
           }
         }
         std::unique_ptr<v8::ScriptCompiler::CachedData> cached_data(
             v8::ScriptCompiler::CreateCodeCache(script->GetUnboundScript()));
-        return std::make_unique<std::vector<uint8_t>>(
-            cached_data->data, cached_data->data + cached_data->length);
+        return std::make_pair(
+            std::make_unique<std::vector<uint8_t>>(
+                cached_data->data, cached_data->data + cached_data->length),
+            /*metadata=*/base::nullopt);
       });
   if (!retrieved_cached_data) {
     return {};
diff --git a/cobalt/script/v8c/v8c_global_environment.h b/cobalt/script/v8c/v8c_global_environment.h
index a422397..ba9e063 100644
--- a/cobalt/script/v8c/v8c_global_environment.h
+++ b/cobalt/script/v8c/v8c_global_environment.h
@@ -62,6 +62,9 @@
       const scoped_refptr<GlobalInterface>& global_interface,
       EnvironmentSettings* environment_settings);
 
+  v8::MaybeLocal<v8::Script> Compile(
+      const scoped_refptr<SourceCode>& source_code) override;
+
   bool EvaluateScript(const scoped_refptr<SourceCode>& script,
                       std::string* out_result_utf8) override;
 
@@ -86,6 +89,8 @@
 
   void EnableEval() override;
 
+  bool IsEvalEnabled() override;
+
   void DisableJit() override;
 
   void SetReportEvalCallback(const base::Closure& report_eval) override;
@@ -152,9 +157,6 @@
   v8::MaybeLocal<v8::Value> EvaluateScriptInternal(
       const scoped_refptr<SourceCode>& source_code);
 
-  v8::MaybeLocal<v8::Script> CompileWithCaching(
-      const scoped_refptr<SourceCode>& source_code);
-
   void EvaluateEmbeddedScript(const unsigned char* data, size_t size,
                               const char* filename);
 
diff --git a/cobalt/script/v8c/v8c_source_code.cc b/cobalt/script/v8c/v8c_source_code.cc
index 477d901..9109a43 100644
--- a/cobalt/script/v8c/v8c_source_code.cc
+++ b/cobalt/script/v8c/v8c_source_code.cc
@@ -21,7 +21,16 @@
 scoped_refptr<SourceCode> SourceCode::CreateSourceCode(
     const std::string& script_utf8, const base::SourceLocation& script_location,
     bool is_muted) {
-  return new v8c::V8cSourceCode(script_utf8, script_location, is_muted);
+  return new v8c::V8cSourceCode(script_utf8, script_location, is_muted,
+                                /*should_cache_compiled_javascript=*/true);
+}
+
+// static
+scoped_refptr<SourceCode> SourceCode::CreateSourceCodeWithoutCaching(
+    const std::string& script_utf8, const base::SourceLocation& script_location,
+    bool is_muted) {
+  return new v8c::V8cSourceCode(script_utf8, script_location, is_muted,
+                                /*should_cache_compiled_javascript=*/false);
 }
 
 }  // namespace script
diff --git a/cobalt/script/v8c/v8c_source_code.h b/cobalt/script/v8c/v8c_source_code.h
index bba30b4..b6fce5e 100644
--- a/cobalt/script/v8c/v8c_source_code.h
+++ b/cobalt/script/v8c/v8c_source_code.h
@@ -31,19 +31,25 @@
  public:
   V8cSourceCode(const std::string& source_utf8,
                 const base::SourceLocation& source_location,
-                bool is_muted = false)
+                bool is_muted = false,
+                bool should_cache_compiled_javascript = true)
       : source_utf8_(source_utf8),
         location_(source_location),
-        is_muted_(is_muted) {}
+        is_muted_(is_muted),
+        should_cache_compiled_javascript_(should_cache_compiled_javascript) {}
 
   const std::string& source_utf8() const { return source_utf8_; }
   const base::SourceLocation& location() const { return location_; }
   bool is_muted() const { return is_muted_; }
+  bool should_cache_compiled_javascript() const {
+    return should_cache_compiled_javascript_;
+  }
 
  private:
   std::string source_utf8_;
   base::SourceLocation location_;
   bool is_muted_;
+  bool should_cache_compiled_javascript_;
 };
 
 }  // namespace v8c
diff --git a/cobalt/script/v8c/v8c_typed_arrays.h b/cobalt/script/v8c/v8c_typed_arrays.h
index e8c9474..82e7509 100644
--- a/cobalt/script/v8c/v8c_typed_arrays.h
+++ b/cobalt/script/v8c/v8c_typed_arrays.h
@@ -37,7 +37,7 @@
   // Default constructor should only be used by bindings code.
   V8cTypedArray() = default;
   V8cTypedArray(v8::Isolate* isolate, v8::Local<v8::Value> value)
-      : isolate_(isolate), ScopedPersistent(isolate, value) {
+      : ScopedPersistent(isolate, value), isolate_(isolate) {
     DCHECK(value->IsTypedArray());
   }
 
@@ -115,8 +115,8 @@
     return;
   }
 
-  *out_typed_array = std::move(
-      V8cUserObjectHolder<V8cTypedArray>(isolate, value.As<v8::TypedArray>()));
+  *out_typed_array =
+      V8cUserObjectHolder<V8cTypedArray>(isolate, value.As<v8::TypedArray>());
 }
 
 template <typename CType, typename BaseTypeName, typename V8Type,
@@ -129,7 +129,7 @@
   // Default constructor should only be used by bindings code.
   V8cTypedArrayImpl() = default;
   V8cTypedArrayImpl(v8::Isolate* isolate, v8::Local<v8::Value> value)
-      : isolate_(isolate), ScopedPersistent(isolate, value) {
+      : ScopedPersistent(isolate, value), isolate_(isolate) {
     DCHECK(((**value).*(V8ValueIsTypeFunction))());
   }
 
@@ -178,42 +178,42 @@
 COBALT_SCRIPT_TYPED_ARRAY_LIST(COBALT_SCRIPT_USING_V8C_ARRAY)
 #undef COBALT_SCRIPT_USING_V8C_ARRAY
 
-#define COBALT_SCRIPT_CONVERSION_BOILERPLATE(array, ctype)                   \
-  template <>                                                                \
-  struct TypeTraits<array> {                                                 \
-    using ConversionType = V8cUserObjectHolder<V8c##array>;                  \
-    using ReturnType = const ScriptValue<array>*;                            \
-  };                                                                         \
-                                                                             \
-  inline void ToJSValue(v8::Isolate* isolate,                                \
-                        const ScriptValue<array>* array_value,               \
-                        v8::Local<v8::Value>* out_value) {                   \
-    if (!array_value) {                                                      \
-      *out_value = v8::Null(isolate);                                        \
-      return;                                                                \
-    }                                                                        \
-    const auto* v8c_array_value =                                            \
-        base::polymorphic_downcast<const V8cUserObjectHolder<V8c##array>*>(  \
-            array_value);                                                    \
-    *out_value = v8c_array_value->v8_value();                                \
-  }                                                                          \
-                                                                             \
-  inline void FromJSValue(v8::Isolate* isolate, v8::Local<v8::Value> value,  \
-                          int conversion_flags,                              \
-                          ExceptionState* exception_state,                   \
-                          V8cUserObjectHolder<V8c##array>* out_array) {      \
-    DCHECK_EQ(0, conversion_flags);                                          \
-    DCHECK(out_array);                                                       \
-    if (!value->IsObject()) {                                                \
-      exception_state->SetSimpleException(kNotObjectType);                   \
-      return;                                                                \
-    }                                                                        \
-    if (!value->Is##array()) {                                               \
-      exception_state->SetSimpleException(kTypeError,                        \
-                                          "Expected object of type array");  \
-      return;                                                                \
-    }                                                                        \
-    *out_array = std::move(V8cUserObjectHolder<V8c##array>(isolate, value)); \
+#define COBALT_SCRIPT_CONVERSION_BOILERPLATE(array, ctype)                  \
+  template <>                                                               \
+  struct TypeTraits<array> {                                                \
+    using ConversionType = V8cUserObjectHolder<V8c##array>;                 \
+    using ReturnType = const ScriptValue<array>*;                           \
+  };                                                                        \
+                                                                            \
+  inline void ToJSValue(v8::Isolate* isolate,                               \
+                        const ScriptValue<array>* array_value,              \
+                        v8::Local<v8::Value>* out_value) {                  \
+    if (!array_value) {                                                     \
+      *out_value = v8::Null(isolate);                                       \
+      return;                                                               \
+    }                                                                       \
+    const auto* v8c_array_value =                                           \
+        base::polymorphic_downcast<const V8cUserObjectHolder<V8c##array>*>( \
+            array_value);                                                   \
+    *out_value = v8c_array_value->v8_value();                               \
+  }                                                                         \
+                                                                            \
+  inline void FromJSValue(v8::Isolate* isolate, v8::Local<v8::Value> value, \
+                          int conversion_flags,                             \
+                          ExceptionState* exception_state,                  \
+                          V8cUserObjectHolder<V8c##array>* out_array) {     \
+    DCHECK_EQ(0, conversion_flags);                                         \
+    DCHECK(out_array);                                                      \
+    if (!value->IsObject()) {                                               \
+      exception_state->SetSimpleException(kNotObjectType);                  \
+      return;                                                               \
+    }                                                                       \
+    if (!value->Is##array()) {                                              \
+      exception_state->SetSimpleException(kTypeError,                       \
+                                          "Expected object of type array"); \
+      return;                                                               \
+    }                                                                       \
+    *out_array = V8cUserObjectHolder<V8c##array>(isolate, value);           \
   }
 COBALT_SCRIPT_TYPED_ARRAY_LIST(COBALT_SCRIPT_CONVERSION_BOILERPLATE)
 #undef COBALT_SCRIPT_CONVERSION_BOILERPLATE
diff --git a/cobalt/site/docs/development/setup-android.md b/cobalt/site/docs/development/setup-android.md
index 2c8bdb5..0382b42 100644
--- a/cobalt/site/docs/development/setup-android.md
+++ b/cobalt/site/docs/development/setup-android.md
@@ -17,6 +17,15 @@
 
 1.  Download and install [Android Studio](https://developer.android.com/studio/).
 
+1.  To enable parallel gradle builds, add the following to your `~/.bashrc`:
+
+    ```
+    export COBALT_GRADLE_BUILD_COUNT=4
+    ```
+
+    Where 4 is the number of parallel threads. You can adjust the number of
+    parallel threads according to how your workstation performs.
+
 1.  Run `cobalt/build/gn.py -p android-x86` to configure the Cobalt build,
     which also installs the SDK and NDK. (This step will have to be repeated
     with 'android-arm' or 'android-arm64' to target those architectures.) The
@@ -214,6 +223,15 @@
 instead of 'cobalt'. Then you can set breakpoints, etc. in the test the same as
 when debugging Cobalt.
 
+## Debugging (Terminal)
+
+Use `adb logcat` while Cobalt is running, or use `adb bugreport` shortly after
+exiting to view Android logs. You will need to filter or search for
+Cobalt-related output.
+
+As with the Linux build, use the `debug`, `devel`, or `qa` configs to trace
+Cobalt's callstacks.
+
 ## Removing the Cobalt Android Environment
 
 1.  Unset ANDROID_HOME and or ANDROID_NDK_HOME in your shell and in .bashrc
diff --git a/cobalt/site/docs/development/setup-linux.md b/cobalt/site/docs/development/setup-linux.md
index 8f570d6..190cac0 100644
--- a/cobalt/site/docs/development/setup-linux.md
+++ b/cobalt/site/docs/development/setup-linux.md
@@ -186,6 +186,14 @@
       </tr>
     </table>
 
+## Debugging Cobalt
+
+`debug`, `devel`, and `qa` configs of Cobalt expose a feature enabling
+developers to trace Cobalt's callstacks per-thread. This is not only a great way
+to debug application performance, but also a great way to debug issues and
+better understand Cobalt's execution flow in general.
+
+Simply build and run one of these configs and observe the terminal output.
 <!--
 <aside class="note">
 <b>Note:</b> If you plan to upload reviews to the Cobalt repository, you
diff --git a/cobalt/version.h b/cobalt/version.h
index 3f59478..7f8ca72 100644
--- a/cobalt/version.h
+++ b/cobalt/version.h
@@ -35,6 +35,6 @@
 //                  release is cut.
 //.
 
-#define COBALT_VERSION "23.lts.1"
+#define COBALT_VERSION "23.lts.2"
 
 #endif  // COBALT_VERSION_H_
diff --git a/cobalt/web/BUILD.gn b/cobalt/web/BUILD.gn
index 4d53f0a..cda07f8 100644
--- a/cobalt/web/BUILD.gn
+++ b/cobalt/web/BUILD.gn
@@ -60,6 +60,13 @@
     "blob.h",
     "buffer_source.cc",
     "buffer_source.h",
+    "cache.cc",
+    "cache.h",
+    "cache_request.h",
+    "cache_storage.cc",
+    "cache_storage.h",
+    "cache_utils.cc",
+    "cache_utils.h",
     "cobalt_ua_data_values_interface.cc",
     "cobalt_ua_data_values_interface.h",
     "context.h",
@@ -71,6 +78,8 @@
     "csp_delegate_factory.h",
     "csp_violation_reporter.cc",
     "csp_violation_reporter.h",
+    "environment_settings_helper.cc",
+    "environment_settings_helper.h",
     "location_base.h",
     "message_event.cc",
     "message_event.h",
@@ -96,12 +105,14 @@
     ":web_events",
     "//cobalt/base",
     "//cobalt/browser:generated_bindings",
+    "//cobalt/cache",
     "//cobalt/csp",
     "//cobalt/network",
     "//cobalt/network_bridge",
     "//cobalt/script",
     "//cobalt/script:engine",
     "//cobalt/script/v8c:engine",
+    "//net",
     "//url",
   ]
 
diff --git a/cobalt/web/blob_test.cc b/cobalt/web/blob_test.cc
index f3cb1b3..6ddc919 100644
--- a/cobalt/web/blob_test.cc
+++ b/cobalt/web/blob_test.cc
@@ -206,7 +206,7 @@
 
 INSTANTIATE_TEST_CASE_P(
     BlobTestsWithJavaScript, BlobTestWithJavaScript,
-    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWorkerTypes()),
+    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWebTypes()),
     testing::TestWebWithJavaScript::GetTypeName);
 
 
diff --git a/cobalt/web/cache.cc b/cobalt/web/cache.cc
new file mode 100644
index 0000000..bbef89f
--- /dev/null
+++ b/cobalt/web/cache.cc
@@ -0,0 +1,507 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/web/cache.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/json/string_escape.h"
+#include "base/memory/ref_counted.h"
+#include "base/strings/string_util.h"
+#include "base/time/time.h"
+#include "cobalt/base/source_location.h"
+#include "cobalt/cache/cache.h"
+#include "cobalt/script/global_environment.h"
+#include "cobalt/script/source_code.h"
+#include "cobalt/script/v8c/conversion_helpers.h"
+#include "cobalt/script/v8c/v8c_user_object_holder.h"
+#include "cobalt/script/v8c/v8c_value_handle.h"
+#include "cobalt/web/cache_utils.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/environment_settings_helper.h"
+#include "net/url_request/url_request_context.h"
+#include "v8/include/v8.h"
+
+namespace cobalt {
+namespace web {
+
+namespace {
+
+const disk_cache::ResourceType kResourceType =
+    disk_cache::ResourceType::kCacheApi;
+
+}  // namespace
+
+Cache::Fetcher::Fetcher(network::NetworkModule* network_module, const GURL& url,
+                        base::OnceCallback<void(bool)> callback)
+    : network_module_(network_module),
+      url_(url),
+      callback_(std::move(callback)),
+      buffer_(new net::GrowableIOBuffer()) {
+  network_module_->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&Cache::Fetcher::Start, base::Unretained(this)));
+}
+
+std::vector<uint8_t> Cache::Fetcher::BufferToVector() const {
+  auto* buffer_begin = reinterpret_cast<const uint8_t*>(buffer_->data());
+  return std::vector<uint8_t>(buffer_begin, buffer_begin + buffer_size_);
+}
+
+std::string Cache::Fetcher::BufferToString() const {
+  return std::string(buffer_->data(), buffer_size_);
+}
+
+void Cache::Fetcher::Start() {
+  request_ = network_module_->url_request_context()->CreateRequest(
+      url_, net::RequestPriority::DEFAULT_PRIORITY, this);
+  request_->Start();
+}
+
+void Cache::Fetcher::Notify(bool success) {
+  // Need to delete |URLRequest| instance on network thread.
+  request_.reset();
+  std::move(callback_).Run(success);
+}
+
+void Cache::Fetcher::OnDone(bool success) {
+  buffer_size_ = buffer_->offset();
+  buffer_->set_offset(0);
+  network_module_->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&Cache::Fetcher::Notify, base::Unretained(this), success));
+}
+
+void Cache::Fetcher::ReadResponse(net::URLRequest* request) {
+  int bytes_read = request->Read(buffer_, buffer_->RemainingCapacity());
+  bool read_running_async = bytes_read == net::ERR_IO_PENDING;
+  bool response_complete = !read_running_async && bytes_read <= 0;
+  // If read is still running, the buffer will be written to and then
+  // |OnReadCompleted()| will be called.
+  if (read_running_async) {
+    return;
+  }
+  if (response_complete) {
+    OnDone(/*success=*/bytes_read == net::OK);
+    return;
+  }
+
+  // Read completed synchronously. Call |OnReadCompleted()| asynchronously to
+  // avoid blocking IO.
+  base::SequencedTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE, base::BindOnce(&Cache::Fetcher::OnReadCompleted,
+                                base::Unretained(this), request, bytes_read));
+}
+
+void Cache::Fetcher::OnResponseStarted(net::URLRequest* request,
+                                       int net_error) {
+  DCHECK_NE(net::ERR_IO_PENDING, net_error);
+  request->GetMimeType(&mime_type_);
+  if (net_error != net::OK) {
+    OnDone(/*success=*/false);
+    return;
+  }
+  int initial_capacity = request->response_headers()->HasHeader(
+                             net::HttpRequestHeaders::kContentLength)
+                             ? request->response_headers()->GetContentLength()
+                             : 64 * 1024;
+  buffer_->SetCapacity(initial_capacity);
+  ReadResponse(request);
+}
+
+void Cache::Fetcher::OnReadCompleted(net::URLRequest* request, int bytes_read) {
+  DCHECK_NE(net::ERR_IO_PENDING, bytes_read);
+  if (bytes_read <= 0) {
+    OnDone(/*success=*/bytes_read == net::OK);
+    return;
+  }
+  // The offset is how much of the buffer is used.
+  buffer_->set_offset(buffer_->offset() + bytes_read);
+  // Grow the buffer, if needed.
+  if (buffer_->RemainingCapacity() == 0) {
+    buffer_->SetCapacity(buffer_->capacity() + 16 * 1024);
+  }
+  ReadResponse(request);
+}
+
+script::HandlePromiseAny Cache::Match(
+    script::EnvironmentSettings* environment_settings,
+    const script::ValueHandleHolder& request) {
+  script::HandlePromiseAny promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<script::Any>();
+  auto promise_reference =
+      std::make_unique<script::ValuePromiseAny::Reference>(this, promise);
+  auto context = get_context(environment_settings);
+  context->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](script::EnvironmentSettings* environment_settings, uint32_t key,
+             std::unique_ptr<script::ValuePromiseAny::Reference>
+                 promise_reference) {
+            auto global_environment =
+                get_global_environment(environment_settings);
+            auto* isolate = global_environment->isolate();
+            auto cached =
+                cache::Cache::GetInstance()->Retrieve(kResourceType, key);
+            if (!cached) {
+              promise_reference->value().Resolve(
+                  cache_utils::GetUndefined(environment_settings));
+              return;
+            }
+            script::v8c::EntryScope entry_scope(isolate);
+            auto response = cache_utils::CreateResponse(environment_settings,
+                                                        std::move(cached));
+            if (!response) {
+              promise_reference->value().Reject();
+            } else {
+              promise_reference->value().Resolve(script::Any(response.value()));
+            }
+          },
+          environment_settings,
+          cache_utils::GetKey(environment_settings, request),
+          std::move(promise_reference)));
+  return promise;
+}
+
+void Cache::PerformAdd(
+    script::EnvironmentSettings* environment_settings,
+    std::unique_ptr<script::ValueHandleHolder::Reference> request_reference,
+    std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference) {
+  auto* global_environment = get_global_environment(environment_settings);
+  auto* isolate = global_environment->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  uint32_t key = cache_utils::GetKey(environment_settings,
+                                     request_reference->referenced_value());
+  if (fetchers_.find(key) != fetchers_.end()) {
+    base::AutoLock auto_lock(*(fetchers_[key]->lock()));
+    auto* promises = &(fetch_contexts_[key].first);
+    promises->push_back(std::move(promise_reference));
+    return;
+  }
+  auto promises =
+      std::vector<std::unique_ptr<script::ValuePromiseVoid::Reference>>();
+  promises.push_back(std::move(promise_reference));
+  fetch_contexts_[key] =
+      std::make_pair(std::move(promises), environment_settings);
+  auto* context = get_context(environment_settings);
+  fetchers_[key] = std::make_unique<Cache::Fetcher>(
+      context->network_module(),
+      GURL(cache_utils::GetUrl(environment_settings,
+                               request_reference->referenced_value())),
+      base::BindOnce(&Cache::OnFetchCompleted, base::Unretained(this), key));
+}
+
+script::HandlePromiseVoid Cache::Add(
+    script::EnvironmentSettings* environment_settings,
+    const script::ValueHandleHolder& request) {
+  auto request_reference =
+      std::make_unique<script::ValueHandleHolder::Reference>(this, request);
+  script::HandlePromiseVoid promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<void>();
+  auto promise_reference =
+      std::make_unique<script::ValuePromiseVoid::Reference>(this, promise);
+  auto context = get_context(environment_settings);
+  context->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&Cache::PerformAdd, base::Unretained(this),
+                     environment_settings, std::move(request_reference),
+                     std::move(promise_reference)));
+  return promise;
+}
+
+script::HandlePromiseVoid Cache::Put(
+    script::EnvironmentSettings* environment_settings,
+    const script::ValueHandleHolder& request,
+    const script::ValueHandleHolder& response) {
+  auto request_reference =
+      std::make_unique<script::ValueHandleHolder::Reference>(this, request);
+  auto response_reference =
+      std::make_unique<script::ValueHandleHolder::Reference>(this, response);
+  script::HandlePromiseVoid promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<void>();
+  auto promise_reference =
+      std::make_unique<script::ValuePromiseVoid::Reference>(this, promise);
+
+  auto context = get_context(environment_settings);
+  base::SequencedTaskRunnerHandle::Get()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](script::EnvironmentSettings* environment_settings,
+             std::unique_ptr<script::ValueHandleHolder::Reference>
+                 request_reference,
+             std::unique_ptr<script::ValueHandleHolder::Reference>
+                 response_reference,
+             std::unique_ptr<script::ValuePromiseVoid::Reference>
+                 promise_reference) {
+
+            auto* global_environment =
+                get_global_environment(environment_settings);
+            auto* isolate = global_environment->isolate();
+            script::v8c::EntryScope entry_scope(isolate);
+            auto context = global_environment->context();
+            auto maybe_body_used = cache_utils::TryGet(
+                context, GetV8Value(response_reference->referenced_value()),
+                "bodyUsed");
+            if (maybe_body_used.IsEmpty() ||
+                maybe_body_used.ToLocalChecked().As<v8::Boolean>()->Value()) {
+              promise_reference->value().Reject(script::kTypeError);
+              return;
+            }
+            auto maybe_text_function = cache_utils::TryGet(
+                context, GetV8Value(response_reference->referenced_value()),
+                "text");
+            if (maybe_text_function.IsEmpty()) {
+              promise_reference->value().Reject();
+              return;
+            }
+            auto text_function = maybe_text_function.ToLocalChecked();
+            v8::Local<v8::Value> text_result;
+            auto response_context =
+                script::GetIsolate(response_reference->referenced_value())
+                    ->GetCurrentContext();
+            if (text_function.IsEmpty() || !text_function->IsFunction() ||
+                !(text_function.As<v8::Function>()
+                      ->Call(response_context,
+                             GetV8Value(response_reference->referenced_value()),
+                             /*argc=*/0,
+                             /*argv=*/nullptr)
+                      .ToLocal(&text_result))) {
+              promise_reference->value().Reject();
+              return;
+            }
+            std::string url = cache_utils::GetUrl(
+                environment_settings, request_reference->referenced_value());
+            auto data = v8::Object::New(isolate);
+            cache_utils::Set(context, data, "environment_settings",
+                             v8::External::New(isolate, environment_settings));
+            cache_utils::Set(
+                context, data, "promise_reference",
+                v8::External::New(isolate, promise_reference.release()));
+            cache_utils::Set(
+                context, data, "request_reference",
+                v8::External::New(isolate, request_reference.release()));
+            auto then_callback =
+                v8::Function::New(
+                    context,
+                    [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+                      auto* isolate = info.GetIsolate();
+                      auto context = info.GetIsolate()->GetCurrentContext();
+                      auto* environment_settings =
+                          static_cast<script::EnvironmentSettings*>(
+                              cache_utils::Get(context, info.Data(),
+                                               "environment_settings")
+                                  .As<v8::External>()
+                                  ->Value());
+                      std::unique_ptr<script::ValueHandleHolder::Reference>
+                      request_reference(
+                          static_cast<script::ValueHandleHolder::Reference*>(
+                              cache_utils::Get(context, info.Data(),
+                                               "request_reference")
+                                  .As<v8::External>()
+                                  ->Value()));
+                      std::unique_ptr<script::ValuePromiseVoid::Reference>
+                      promise_reference(
+                          static_cast<script::ValuePromiseVoid::Reference*>(
+                              cache_utils::Get(context, info.Data(),
+                                               "promise_reference")
+                                  .As<v8::External>()
+                                  ->Value()));
+                      uint32_t key = cache_utils::GetKey(
+                          environment_settings,
+                          request_reference->referenced_value());
+                      std::string url = cache_utils::GetUrl(
+                          environment_settings,
+                          request_reference->referenced_value());
+                      std::string body;
+                      FromJSValue(info.GetIsolate(), info[0],
+                                  script::v8c::kNoConversionFlags, nullptr,
+                                  &body);
+                      auto* begin =
+                          reinterpret_cast<const uint8_t*>(body.data());
+                      auto data = std::make_unique<std::vector<uint8_t>>(
+                          begin, begin + body.size());
+                      cache::Cache::GetInstance()->Store(
+                          kResourceType, key, *data, base::Value(url));
+                      promise_reference->value().Resolve();
+                    },
+                    data)
+                    .ToLocalChecked();
+            if (text_result.As<v8::Promise>()
+                    ->Then(context, then_callback)
+                    .IsEmpty()) {
+              promise_reference->value().Reject();
+              return;
+            }
+            auto catch_callback =
+                v8::Function::New(
+                    context,
+                    [](const v8::FunctionCallbackInfo<v8::Value>& info) {
+                      auto* isolate = info.GetIsolate();
+                      auto context = info.GetIsolate()->GetCurrentContext();
+                      std::unique_ptr<script::ValuePromiseVoid::Reference>
+                      promise_reference(
+                          static_cast<script::ValuePromiseVoid::Reference*>(
+                              cache_utils::Get(context, info.Data(),
+                                               "promise_reference")
+                                  .As<v8::External>()
+                                  ->Value()));
+                      promise_reference->value().Reject();
+                    },
+                    data)
+                    .ToLocalChecked();
+            if (text_result.As<v8::Promise>()
+                    ->Catch(context, catch_callback)
+                    .IsEmpty()) {
+              promise_reference->value().Reject();
+              return;
+            }
+            // Run |response.text()| promise.
+            isolate->PerformMicrotaskCheckpoint();
+          },
+          environment_settings, std::move(request_reference),
+          std::move(response_reference), std::move(promise_reference)));
+  return promise;
+}
+
+script::HandlePromiseBool Cache::Delete(
+    script::EnvironmentSettings* environment_settings,
+    const script::ValueHandleHolder& request) {
+  script::HandlePromiseBool promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<bool>();
+  auto request_reference =
+      std::make_unique<script::ValueHandleHolder::Reference>(this, request);
+  auto promise_reference =
+      std::make_unique<script::ValuePromiseBool::Reference>(this, promise);
+  auto context = get_context(environment_settings);
+  context->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](script::EnvironmentSettings* environment_settings,
+             std::unique_ptr<script::ValueHandleHolder::Reference>
+                 request_reference,
+             std::unique_ptr<script::ValuePromiseBool::Reference>
+                 promise_reference) {
+            auto* global_environment =
+                get_global_environment(environment_settings);
+            auto* isolate = global_environment->isolate();
+            script::v8c::EntryScope entry_scope(isolate);
+            promise_reference->value().Resolve(
+                cache::Cache::GetInstance()->Delete(
+                    kResourceType, cache_utils::GetKey(
+                                       environment_settings,
+                                       request_reference->referenced_value())));
+          },
+          environment_settings, std::move(request_reference),
+          std::move(promise_reference)));
+  return promise;
+}
+
+script::HandlePromiseAny Cache::Keys(
+    script::EnvironmentSettings* environment_settings) {
+  script::HandlePromiseAny promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<script::Any>();
+  auto promise_reference =
+      std::make_unique<script::ValuePromiseAny::Reference>(this, promise);
+  auto context = get_context(environment_settings);
+  context->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](script::EnvironmentSettings* environment_settings,
+             std::unique_ptr<script::ValuePromiseAny::Reference>
+                 promise_reference) {
+            auto* global_environment =
+                get_global_environment(environment_settings);
+            auto* isolate = global_environment->isolate();
+            script::v8c::EntryScope entry_scope(isolate);
+            auto keys =
+                cache::Cache::GetInstance()->KeysWithMetadata(kResourceType);
+            std::vector<v8::Local<v8::Value>> requests;
+            for (uint8_t key :
+                 cache::Cache::GetInstance()->KeysWithMetadata(kResourceType)) {
+              std::unique_ptr<base::Value> url =
+                  cache::Cache::GetInstance()->Metadata(kResourceType, key);
+              if (url && url->is_string()) {
+                base::Optional<script::Any> request =
+                    cache_utils::CreateRequest(environment_settings,
+                                               url->GetString());
+                if (request) {
+                  requests.push_back(GetV8Value(*(request->GetScriptValue())));
+                }
+              }
+            }
+            promise_reference->value().Resolve(
+                script::Any(new script::v8c::V8cValueHandleHolder(
+                    isolate, v8::Array::New(isolate, requests.data(),
+                                            requests.size()))));
+
+          },
+          environment_settings, std::move(promise_reference)));
+  return promise;
+}
+
+void Cache::OnFetchCompleted(uint32_t key, bool success) {
+  auto* environment_settings = fetch_contexts_[key].second;
+  auto* context = get_context(environment_settings);
+  context->message_loop()->task_runner()->PostTask(
+      FROM_HERE, base::BindOnce(&Cache::OnFetchCompletedMainThread,
+                                base::Unretained(this), key, success));
+}
+
+void Cache::OnFetchCompletedMainThread(uint32_t key, bool success) {
+  auto* fetcher = fetchers_[key].get();
+  auto* promises = &(fetch_contexts_[key].first);
+  if (!success) {
+    {
+      base::AutoLock auto_lock(*fetcher->lock());
+      while (promises->size() > 0) {
+        promises->back()->value().Reject();
+        promises->pop_back();
+      }
+    }
+    fetchers_.erase(key);
+    fetch_contexts_.erase(key);
+    return;
+  }
+  {
+    cache::Cache::GetInstance()->Store(kResourceType, key,
+                                       fetcher->BufferToVector(),
+                                       base::Value(fetcher->url().spec()));
+    if (fetcher->mime_type() == "text/javascript") {
+      auto* environment_settings = fetch_contexts_[key].second;
+      auto* global_environment = get_global_environment(environment_settings);
+      auto* isolate = global_environment->isolate();
+      script::v8c::EntryScope entry_scope(isolate);
+      global_environment->Compile(script::SourceCode::CreateSourceCode(
+          fetcher->BufferToString(), base::SourceLocation(__FILE__, 1, 1)));
+    }
+    base::AutoLock auto_lock(*fetcher->lock());
+    while (promises->size() > 0) {
+      promises->back()->value().Resolve();
+      promises->pop_back();
+    }
+  }
+  fetchers_.erase(key);
+  fetch_contexts_.erase(key);
+}
+
+}  // namespace web
+}  // namespace cobalt
diff --git a/cobalt/web/cache.h b/cobalt/web/cache.h
new file mode 100644
index 0000000..ee09fb7
--- /dev/null
+++ b/cobalt/web/cache.h
@@ -0,0 +1,112 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_CACHE_H_
+#define COBALT_WEB_CACHE_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "base/optional.h"
+#include "base/synchronization/lock.h"
+#include "cobalt/network/network_module.h"
+#include "cobalt/script/environment_settings.h"
+#include "cobalt/script/script_value_factory.h"
+#include "cobalt/script/sequence.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/script/wrappable.h"
+#include "net/base/io_buffer.h"
+#include "net/url_request/url_request.h"
+#include "url/gurl.h"
+
+namespace cobalt {
+namespace web {
+
+class Cache : public script::Wrappable {
+ public:
+  Cache() = default;
+
+  // Web API: Cache
+  //
+  script::Handle<script::Promise<script::Handle<script::ValueHandle>>> Match(
+      script::EnvironmentSettings* environment_settings,
+      const script::ValueHandleHolder& request);
+  script::HandlePromiseVoid Add(
+      script::EnvironmentSettings* environment_settings,
+      const script::ValueHandleHolder& request);
+  script::HandlePromiseVoid Put(
+      script::EnvironmentSettings* environment_settings,
+      const script::ValueHandleHolder& request,
+      const script::ValueHandleHolder& response);
+  script::HandlePromiseBool Delete(
+      script::EnvironmentSettings* environment_settings,
+      const script::ValueHandleHolder& request);
+  script::Handle<script::Promise<script::Handle<script::ValueHandle>>> Keys(
+      script::EnvironmentSettings* environment_settings);
+
+  DEFINE_WRAPPABLE_TYPE(Cache);
+
+ private:
+  class Fetcher : public net::URLRequest::Delegate {
+   public:
+    Fetcher(network::NetworkModule* network_module, const GURL& url,
+            base::OnceCallback<void(bool)> callback);
+
+    // net::URLRequest::Delegate implementations.
+    void OnResponseStarted(net::URLRequest* request, int net_error) override;
+    void OnReadCompleted(net::URLRequest* request, int bytes_read) override;
+
+    const std::string& mime_type() const { return mime_type_; }
+    GURL url() const { return url_; }
+    base::Lock* lock() const { return &lock_; }
+    std::vector<uint8_t> BufferToVector() const;
+    std::string BufferToString() const;
+
+   private:
+    void Notify(bool success);
+    void OnDone(bool success);
+    void ReadResponse(net::URLRequest* request);
+    void Start();
+
+    network::NetworkModule* network_module_;
+    GURL url_;
+    base::OnceCallback<void(bool)> callback_;
+    std::unique_ptr<net::URLRequest> request_;
+    std::string mime_type_;
+    scoped_refptr<net::GrowableIOBuffer> buffer_;
+    int buffer_size_;
+    mutable base::Lock lock_;
+  };
+
+  void PerformAdd(
+      script::EnvironmentSettings* environment_settings,
+      std::unique_ptr<script::ValueHandleHolder::Reference> request_reference,
+      std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
+  void OnFetchCompleted(uint32_t key, bool success);
+  void OnFetchCompletedMainThread(uint32_t key, bool success);
+
+  std::map<uint32_t, std::unique_ptr<Fetcher>> fetchers_;
+  std::map<uint32_t, std::pair<std::vector<std::unique_ptr<
+                                   script::ValuePromiseVoid::Reference>>,
+                               script::EnvironmentSettings*>>
+      fetch_contexts_;
+};
+
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_CACHE_H_
diff --git a/cobalt/web/cache.idl b/cobalt/web/cache.idl
new file mode 100644
index 0000000..87ea59a
--- /dev/null
+++ b/cobalt/web/cache.idl
@@ -0,0 +1,27 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://w3c.github.io/ServiceWorker/#cache-interface
+
+[Exposed=(Window,Worker)]
+interface Cache {
+  // |match| returns a new |Response| object. |Response| is polyfilled, so
+  // return object of type |any|. When promise is resovled with |undefined|,
+  // the cache does not have an entry for the given |request|.
+  [CallWith=EnvironmentSettings, NewObject] Promise<any> match(any request);
+  [CallWith=EnvironmentSettings, NewObject] Promise<void> add(any request);
+  [CallWith=EnvironmentSettings, NewObject] Promise<void> put(any request, any response);
+  [CallWith=EnvironmentSettings, NewObject] Promise<boolean> delete(any request);
+  [CallWith=EnvironmentSettings, NewObject] Promise<any> keys();
+};
diff --git a/cobalt/web/cache_request.h b/cobalt/web/cache_request.h
new file mode 100644
index 0000000..b5fb6b8
--- /dev/null
+++ b/cobalt/web/cache_request.h
@@ -0,0 +1,38 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_CACHE_REQUEST_H_
+#define COBALT_WEB_CACHE_REQUEST_H_
+
+#include <string>
+
+namespace cobalt {
+namespace web {
+
+class CacheRequest : public script::Wrappable {
+ public:
+  explicit CacheRequest(const std::string& input) : url_(input) {}
+
+  const std::string& url() const { return url_; }
+
+  DEFINE_WRAPPABLE_TYPE(CacheRequest);
+
+ private:
+  const std::string url_;
+};
+
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_CACHE_REQUEST_H_
diff --git a/cobalt/web/cache_request.idl b/cobalt/web/cache_request.idl
new file mode 100644
index 0000000..7f9ccdb
--- /dev/null
+++ b/cobalt/web/cache_request.idl
@@ -0,0 +1,23 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://w3c.github.io/ServiceWorker/#cache-interface
+
+[
+  Exposed=(Window,Worker),
+  Constructor(USVString input),
+]
+interface CacheRequest {
+  readonly attribute USVString url;
+};
diff --git a/cobalt/web/cache_storage.cc b/cobalt/web/cache_storage.cc
new file mode 100644
index 0000000..db96ded
--- /dev/null
+++ b/cobalt/web/cache_storage.cc
@@ -0,0 +1,94 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/web/cache_storage.h"
+
+#include <memory>
+#include <string>
+#include <utility>
+
+#include "base/bind.h"
+#include "cobalt/base/source_location.h"
+#include "cobalt/cache/cache.h"
+#include "cobalt/script/source_code.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/environment_settings_helper.h"
+
+namespace cobalt {
+namespace web {
+
+script::Handle<script::Promise<script::Handle<script::ValueHandle>>>
+CacheStorage::Match(script::EnvironmentSettings* environment_settings,
+                    const script::ValueHandleHolder& request) {
+  return GetOrCreateCache()->Match(environment_settings, request);
+}
+
+script::Handle<script::Promise<script::Handle<script::ValueHandle>>>
+CacheStorage::Match(script::EnvironmentSettings* environment_settings,
+                    const script::ValueHandleHolder& request,
+                    const MultiCacheQueryOptions& options) {
+  return Match(environment_settings, request);
+}
+
+void CacheStorage::PerformOpen(
+    std::unique_ptr<script::ValuePromiseWrappable::Reference>
+        promise_reference) {
+  promise_reference->value().Resolve(GetOrCreateCache());
+}
+
+script::HandlePromiseWrappable CacheStorage::Open(
+    script::EnvironmentSettings* environment_settings,
+    const std::string& cache_name) {
+  script::HandlePromiseWrappable promise =
+      get_script_value_factory(environment_settings)
+          ->CreateInterfacePromise<scoped_refptr<Cache>>();
+  auto* context = get_context(environment_settings);
+  context->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&CacheStorage::PerformOpen, base::Unretained(this),
+                     std::make_unique<script::ValuePromiseWrappable::Reference>(
+                         this, promise)));
+  return promise;
+}
+
+script::HandlePromiseBool CacheStorage::Delete(
+    script::EnvironmentSettings* environment_settings,
+    const std::string& cache_name) {
+  script::HandlePromiseBool promise =
+      get_script_value_factory(environment_settings)
+          ->CreateBasicPromise<bool>();
+  auto promise_reference =
+      std::make_unique<script::ValuePromiseBool::Reference>(this, promise);
+  auto* context = get_context(environment_settings);
+  context->message_loop()->task_runner()->PostTask(
+      FROM_HERE, base::BindOnce(
+                     [](std::unique_ptr<script::ValuePromiseBool::Reference>
+                            promise_reference) {
+                       cache::Cache::GetInstance()->Delete(
+                           disk_cache::ResourceType::kCacheApi);
+                       promise_reference->value().Resolve(true);
+                     },
+                     std::move(promise_reference)));
+  return promise;
+}
+
+scoped_refptr<Cache> CacheStorage::GetOrCreateCache() {
+  if (!cache_) {
+    cache_ = new Cache();
+  }
+  return cache_;
+}
+
+}  // namespace web
+}  // namespace cobalt
diff --git a/cobalt/web/cache_storage.h b/cobalt/web/cache_storage.h
new file mode 100644
index 0000000..ec25b1b
--- /dev/null
+++ b/cobalt/web/cache_storage.h
@@ -0,0 +1,66 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_CACHE_STORAGE_H_
+#define COBALT_WEB_CACHE_STORAGE_H_
+
+#include <map>
+#include <memory>
+#include <string>
+
+#include "base/memory/ref_counted.h"
+#include "cobalt/script/environment_settings.h"
+#include "cobalt/script/script_value_factory.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/script/wrappable.h"
+#include "cobalt/web/cache.h"
+#include "cobalt/web/multi_cache_query_options.h"
+
+namespace cobalt {
+namespace web {
+
+class CacheStorage : public script::Wrappable {
+ public:
+  CacheStorage() = default;
+
+  // Web API: CacheStorage
+  //
+  script::Handle<script::Promise<script::Handle<script::ValueHandle>>> Match(
+      script::EnvironmentSettings* environment_settings,
+      const script::ValueHandleHolder& request);
+  script::Handle<script::Promise<script::Handle<script::ValueHandle>>> Match(
+      script::EnvironmentSettings* environment_settings,
+      const script::ValueHandleHolder& request,
+      const MultiCacheQueryOptions& options);
+  script::HandlePromiseWrappable Open(
+      script::EnvironmentSettings* environment_settings,
+      const std::string& cache_name);
+  script::HandlePromiseBool Delete(
+      script::EnvironmentSettings* environment_settings,
+      const std::string& cache_name);
+
+  DEFINE_WRAPPABLE_TYPE(CacheStorage);
+
+ private:
+  scoped_refptr<Cache> GetOrCreateCache();
+  void PerformOpen(std::unique_ptr<script::ValuePromiseWrappable::Reference>
+                       promise_reference);
+
+  scoped_refptr<Cache> cache_;
+};
+
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_CACHE_STORAGE_H_
diff --git a/cobalt/web/cache_storage.idl b/cobalt/web/cache_storage.idl
new file mode 100644
index 0000000..a9744a6
--- /dev/null
+++ b/cobalt/web/cache_storage.idl
@@ -0,0 +1,25 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://w3c.github.io/ServiceWorker/#cachestorage
+
+// TODO(b/250611661): implement complete Cache API and adhere to web spec.
+
+[Exposed=(Window,Worker)]
+interface CacheStorage {
+  [CallWith=EnvironmentSettings, NewObject] Promise<any> match(any request, optional MultiCacheQueryOptions options);
+  [CallWith=EnvironmentSettings, NewObject] Promise<Cache> open(DOMString cacheName);
+  // Ignores |cacheName| and deletes all Cache API data.
+  [CallWith=EnvironmentSettings, NewObject] Promise<boolean> delete(DOMString cacheName);
+};
diff --git a/cobalt/web/cache_utils.cc b/cobalt/web/cache_utils.cc
new file mode 100644
index 0000000..3026127
--- /dev/null
+++ b/cobalt/web/cache_utils.cc
@@ -0,0 +1,157 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/web/cache_utils.h"
+
+#include "cobalt/cache/cache.h"
+#include "cobalt/script/v8c/conversion_helpers.h"
+#include "cobalt/web/environment_settings_helper.h"
+
+namespace cobalt {
+namespace web {
+namespace cache_utils {
+
+v8::Local<v8::String> V8String(v8::Isolate* isolate, const std::string& s) {
+  return v8::String::NewFromUtf8(isolate, s.c_str()).ToLocalChecked();
+}
+
+v8::MaybeLocal<v8::Value> TryGet(v8::Local<v8::Context> context,
+                                 v8::Local<v8::Value> object,
+                                 const std::string& key) {
+  if (!object->IsObject()) {
+    return v8::MaybeLocal<v8::Value>();
+  }
+  auto* isolate = context->GetIsolate();
+  return object.As<v8::Object>()->Get(context, V8String(isolate, key));
+}
+
+v8::Local<v8::Value> Get(v8::Local<v8::Context> context,
+                         v8::Local<v8::Value> object, const std::string& key) {
+  return TryGet(context, object, key).ToLocalChecked();
+}
+
+bool Set(v8::Local<v8::Context> context, v8::Local<v8::Value> object,
+         const std::string& key, v8::Local<v8::Value> value) {
+  if (!object->IsObject()) {
+    return false;
+  }
+  auto* isolate = context->GetIsolate();
+  auto result =
+      object.As<v8::Object>()->Set(context, V8String(isolate, key), value);
+  return !result.IsNothing();
+}
+
+script::Any GetUndefined(script::EnvironmentSettings* environment_settings) {
+  auto* global_environment = get_global_environment(environment_settings);
+  auto* isolate = global_environment->isolate();
+  return script::Any(
+      new script::v8c::V8cValueHandleHolder(isolate, v8::Undefined(isolate)));
+}
+
+script::Any EvaluateString(script::EnvironmentSettings* environment_settings,
+                           const std::string& js_code) {
+  auto* global_environment = get_global_environment(environment_settings);
+  auto* wrappable = get_global_wrappable(environment_settings);
+  base::Optional<script::ValueHandleHolder::Reference> reference;
+  scoped_refptr<script::SourceCode> source_code =
+      script::SourceCode::CreateSourceCodeWithoutCaching(
+          js_code, base::SourceLocation(__FILE__, __LINE__, 1));
+  bool eval_enabled = global_environment->IsEvalEnabled();
+  if (!eval_enabled) {
+    global_environment->EnableEval();
+  }
+  bool success =
+      global_environment->EvaluateScript(source_code, wrappable, &reference);
+  if (!eval_enabled) {
+    global_environment->DisableEval("");
+  }
+  if (success && reference) {
+    return script::Any(reference.value());
+  } else {
+    return GetUndefined(environment_settings);
+  }
+}
+
+base::Optional<script::Any> CreateInstance(
+    script::EnvironmentSettings* environment_settings,
+    const std::string& class_name, int argc, v8::Local<v8::Value> argv[]) {
+  auto* global_environment = get_global_environment(environment_settings);
+  auto* isolate = global_environment->isolate();
+  auto reponse_function =
+      cache_utils::EvaluateString(environment_settings, class_name);
+  auto v8_function =
+      script::GetV8Value(*reponse_function.GetScriptValue()).As<v8::Function>();
+  auto context = isolate->GetCurrentContext();
+  auto maybe_instance = v8_function->NewInstance(context, argc, argv);
+  if (maybe_instance.IsEmpty()) {
+    return base::nullopt;
+  }
+  return script::Any(new script::v8c::V8cValueHandleHolder(
+      isolate, maybe_instance.ToLocalChecked()));
+}
+
+base::Optional<script::Any> CreateRequest(
+    script::EnvironmentSettings* environment_settings, const std::string& url) {
+  auto* global_environment = get_global_environment(environment_settings);
+  auto* isolate = global_environment->isolate();
+  v8::Local<v8::Value> argv[] = {V8String(isolate, url)};
+  return CreateInstance(environment_settings, "Response", /*argc=*/1, argv);
+}
+
+base::Optional<script::Any> CreateResponse(
+    script::EnvironmentSettings* environment_settings,
+    std::unique_ptr<std::vector<uint8_t>> data) {
+  auto* global_environment = get_global_environment(environment_settings);
+  auto* isolate = global_environment->isolate();
+  auto array_buffer = v8::ArrayBuffer::New(isolate, data->size());
+  memcpy(array_buffer->GetBackingStore()->Data(), data->data(), data->size());
+  v8::Local<v8::Value> argv[] = {array_buffer};
+  return CreateInstance(environment_settings, "Response", /*argc=*/1, argv);
+}
+
+uint32_t GetKey(const std::string& url) { return cache::Cache::CreateKey(url); }
+
+uint32_t GetKey(script::EnvironmentSettings* environment_settings,
+                const script::ValueHandleHolder& request_info) {
+  return GetKey(GetUrl(environment_settings, request_info));
+}
+
+std::string GetUrl(script::EnvironmentSettings* environment_settings,
+                   const script::ValueHandleHolder& request_info) {
+  auto v8_value = GetV8Value(request_info);
+  auto* isolate = GetIsolate(request_info);
+  v8::Local<v8::String> v8_string;
+  if (v8_value->IsString()) {
+    v8_string = v8_value.As<v8::String>();
+  } else {
+    auto context = isolate->GetCurrentContext();
+    // Treat like |Request| and get "url" property.
+    v8_string = Get(context, v8_value, "url").As<v8::String>();
+  }
+  std::string url;
+  FromJSValue(isolate, v8_string, script::v8c::kNoConversionFlags, nullptr,
+              &url);
+  GURL::Replacements replacements;
+  replacements.ClearUsername();
+  replacements.ClearPassword();
+  replacements.ClearRef();
+  return environment_settings->base_url()
+      .Resolve(url)
+      .ReplaceComponents(replacements)
+      .spec();
+}
+
+}  // namespace cache_utils
+}  // namespace web
+}  // namespace cobalt
diff --git a/cobalt/web/cache_utils.h b/cobalt/web/cache_utils.h
new file mode 100644
index 0000000..983895f
--- /dev/null
+++ b/cobalt/web/cache_utils.h
@@ -0,0 +1,69 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_CACHE_UTILS_H_
+#define COBALT_WEB_CACHE_UTILS_H_
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "cobalt/script/environment_settings.h"
+#include "cobalt/script/script_value_factory.h"
+#include "cobalt/script/value_handle.h"
+#include "v8/include/v8.h"
+
+namespace cobalt {
+namespace web {
+namespace cache_utils {
+
+v8::Local<v8::String> V8String(v8::Isolate* isolate, const std::string& s);
+
+v8::MaybeLocal<v8::Value> TryGet(v8::Local<v8::Context> context,
+                                 v8::Local<v8::Value> object,
+                                 const std::string& key);
+
+v8::Local<v8::Value> Get(v8::Local<v8::Context> context,
+                         v8::Local<v8::Value> object, const std::string& key);
+
+bool Set(v8::Local<v8::Context> context, v8::Local<v8::Value> object,
+         const std::string& key, v8::Local<v8::Value> value);
+
+script::Any GetUndefined(script::EnvironmentSettings* environment_settings);
+
+script::Any EvaluateString(script::EnvironmentSettings* environment_settings,
+                           const std::string& js_code);
+
+base::Optional<script::Any> CreateInstance(
+    script::EnvironmentSettings* environment_settings,
+    const std::string& class_name, int argc, v8::Local<v8::Value> argv[]);
+base::Optional<script::Any> CreateRequest(
+    script::EnvironmentSettings* environment_settings, const std::string& url);
+base::Optional<script::Any> CreateResponse(
+    script::EnvironmentSettings* environment_settings,
+    std::unique_ptr<std::vector<uint8_t>> data);
+
+uint32_t GetKey(const std::string& url);
+
+uint32_t GetKey(script::EnvironmentSettings* environment_settings,
+                const script::ValueHandleHolder& request_info);
+
+std::string GetUrl(script::EnvironmentSettings* environment_settings,
+                   const script::ValueHandleHolder& request_info);
+
+}  // namespace cache_utils
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_CACHE_UTILS_H_
diff --git a/cobalt/web/csp_violation_reporter.cc b/cobalt/web/csp_violation_reporter.cc
index 2cb1951..63d2cbd 100644
--- a/cobalt/web/csp_violation_reporter.cc
+++ b/cobalt/web/csp_violation_reporter.cc
@@ -134,22 +134,16 @@
   ViolationEvent violation_data;
   GatherSecurityPolicyViolationEventData(global_, violation_info,
                                          &violation_data);
+  EventTarget* target = global_;
   if (global_->IsWindow()) {
-    global_->AsWindow()->document()->DispatchEvent(
-        new SecurityPolicyViolationEvent(
-            violation_data.document_uri, violation_data.referrer,
-            violation_data.blocked_uri, violation_data.violated_directive,
-            violation_data.effective_directive, violation_data.original_policy,
-            violation_data.source_file, violation_data.status_code,
-            violation_data.line_number, violation_data.column_number));
-  } else {
-    global_->DispatchEvent(new SecurityPolicyViolationEvent(
-        violation_data.document_uri, violation_data.referrer,
-        violation_data.blocked_uri, violation_data.violated_directive,
-        violation_data.effective_directive, violation_data.original_policy,
-        violation_data.source_file, violation_data.status_code,
-        violation_data.line_number, violation_data.column_number));
+    target = global_->AsWindow()->document();
   }
+  target->DispatchEvent(new SecurityPolicyViolationEvent(
+      violation_data.document_uri, violation_data.referrer,
+      violation_data.blocked_uri, violation_data.violated_directive,
+      violation_data.effective_directive, violation_data.original_policy,
+      violation_data.source_file, violation_data.status_code,
+      violation_data.line_number, violation_data.column_number));
 
   if (violation_info.endpoints.empty() || post_sender_.is_null()) {
     return;
diff --git a/cobalt/web/custom_event_test.cc b/cobalt/web/custom_event_test.cc
index 9bf87ae..5d1bded 100644
--- a/cobalt/web/custom_event_test.cc
+++ b/cobalt/web/custom_event_test.cc
@@ -109,7 +109,7 @@
 
 INSTANTIATE_TEST_CASE_P(
     CustomEventTestsWithJavaScript, CustomEventTestWithJavaScript,
-    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWorkerTypes()),
+    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWebTypes()),
     testing::TestWebWithJavaScript::GetTypeName);
 
 }  // namespace web
diff --git a/cobalt/web/environment_settings.h b/cobalt/web/environment_settings.h
index 6301971..c4a0c18 100644
--- a/cobalt/web/environment_settings.h
+++ b/cobalt/web/environment_settings.h
@@ -20,6 +20,9 @@
 
 #include "cobalt/base/debugger_hooks.h"
 #include "cobalt/script/environment_settings.h"
+#include "cobalt/script/global_environment.h"
+#include "cobalt/script/script_value_factory.h"
+#include "cobalt/script/wrappable.h"
 #include "url/origin.h"
 
 namespace cobalt {
@@ -37,15 +40,20 @@
   ~EnvironmentSettings() override {}
 
   // https://html.spec.whatwg.org/multipage/webappapis.html#realm-execution-context
-  Context* context() const {
-    DCHECK(context_);
-    return context_;
-  }
+  Context* context() const { return context_; }
   void set_context(Context* context) { context_ = context; }
 
   // https://storage.spec.whatwg.org/#obtain-a-storage-key
   url::Origin ObtainStorageKey() { return url::Origin::Create(creation_url()); }
 
+  static Context* context(script::EnvironmentSettings* environment_settings);
+  static script::GlobalEnvironment* global_environment(
+      script::EnvironmentSettings* environment_settings);
+  static script::Wrappable* global_wrappable(
+      script::EnvironmentSettings* environment_settings);
+  static script::ScriptValueFactory* script_value_factory(
+      script::EnvironmentSettings* environment_settings);
+
  protected:
   friend std::unique_ptr<EnvironmentSettings>::deleter_type;
 
diff --git a/cobalt/web/environment_settings_helper.cc b/cobalt/web/environment_settings_helper.cc
new file mode 100644
index 0000000..4a87c2e
--- /dev/null
+++ b/cobalt/web/environment_settings_helper.cc
@@ -0,0 +1,46 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/web/environment_settings_helper.h"
+
+#include "cobalt/base/polymorphic_downcast.h"
+#include "cobalt/web/context.h"
+#include "cobalt/web/environment_settings.h"
+
+namespace cobalt {
+namespace web {
+
+Context* get_context(script::EnvironmentSettings* environment_settings) {
+  auto* settings =
+      base::polymorphic_downcast<EnvironmentSettings*>(environment_settings);
+  return settings->context();
+}
+
+script::GlobalEnvironment* get_global_environment(
+    script::EnvironmentSettings* environment_settings) {
+  return get_context(environment_settings)->global_environment();
+}
+
+script::Wrappable* get_global_wrappable(
+    script::EnvironmentSettings* environment_settings) {
+  return get_global_environment(environment_settings)->global_wrappable();
+}
+
+script::ScriptValueFactory* get_script_value_factory(
+    script::EnvironmentSettings* environment_settings) {
+  return get_global_environment(environment_settings)->script_value_factory();
+}
+
+}  // namespace web
+}  // namespace cobalt
diff --git a/cobalt/web/environment_settings_helper.h b/cobalt/web/environment_settings_helper.h
new file mode 100644
index 0000000..ca1b83e
--- /dev/null
+++ b/cobalt/web/environment_settings_helper.h
@@ -0,0 +1,37 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#ifndef COBALT_WEB_ENVIRONMENT_SETTINGS_HELPER_H_
+#define COBALT_WEB_ENVIRONMENT_SETTINGS_HELPER_H_
+
+#include "cobalt/script/environment_settings.h"
+#include "cobalt/script/global_environment.h"
+#include "cobalt/script/script_value_factory.h"
+#include "cobalt/web/context.h"
+
+namespace cobalt {
+namespace web {
+
+Context* get_context(script::EnvironmentSettings* environment_settings);
+script::GlobalEnvironment* get_global_environment(
+    script::EnvironmentSettings* environment_settings);
+script::Wrappable* get_global_wrappable(
+    script::EnvironmentSettings* environment_settings);
+script::ScriptValueFactory* get_script_value_factory(
+    script::EnvironmentSettings* environment_settings);
+
+}  // namespace web
+}  // namespace cobalt
+
+#endif  // COBALT_WEB_ENVIRONMENT_SETTINGS_HELPER_H_
diff --git a/cobalt/web/event.cc b/cobalt/web/event.cc
index f791fef..46dece3 100644
--- a/cobalt/web/event.cc
+++ b/cobalt/web/event.cc
@@ -26,27 +26,13 @@
   InitEventInternal(base::Token(), false, false);
 }
 
-Event::Event(const std::string& type)
-    : event_phase_(kNone), time_stamp_(GetEventTime(SbTimeGetMonotonicNow())) {
-  InitEventInternal(base::Token(type), false, false);
-}
-
-Event::Event(const std::string& type, const EventInit& init_dict)
-    : event_phase_(kNone), time_stamp_(GetEventTime(SbTimeGetMonotonicNow())) {
-  SB_DCHECK(init_dict.has_bubbles());
-  SB_DCHECK(init_dict.has_cancelable());
-  if (init_dict.time_stamp() != 0) {
-    time_stamp_ = init_dict.time_stamp();
-  }
-  InitEventInternal(base::Token(type), init_dict.bubbles(),
-                    init_dict.cancelable());
-}
-
+Event::Event(const std::string& type) : Event(base::Token(type)) {}
 Event::Event(base::Token type)
     : event_phase_(kNone), time_stamp_(GetEventTime(SbTimeGetMonotonicNow())) {
   InitEventInternal(type, false, false);
 }
-
+Event::Event(const std::string& type, const EventInit& init_dict)
+    : Event(base::Token(type), init_dict) {}
 Event::Event(base::Token type, Bubbles bubbles, Cancelable cancelable)
     : event_phase_(kNone), time_stamp_(GetEventTime(SbTimeGetMonotonicNow())) {
   InitEventInternal(type, bubbles == kBubbles, cancelable == kCancelable);
diff --git a/cobalt/web/global_cache_storage.idl b/cobalt/web/global_cache_storage.idl
new file mode 100644
index 0000000..702faa5
--- /dev/null
+++ b/cobalt/web/global_cache_storage.idl
@@ -0,0 +1,22 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://w3c.github.io/ServiceWorker/#self-caches
+
+[NoInterfaceObject]
+interface GlobalCacheStorage {
+  readonly attribute CacheStorage caches;
+};
+Window implements GlobalCacheStorage;
+WorkerGlobalScope implements GlobalCacheStorage;
diff --git a/cobalt/web/message_event.cc b/cobalt/web/message_event.cc
index a6e8888..b38a1fe 100644
--- a/cobalt/web/message_event.cc
+++ b/cobalt/web/message_event.cc
@@ -29,17 +29,36 @@
 #include "starboard/memory.h"
 #include "v8/include/v8.h"
 
+namespace cobalt {
+namespace web {
+
 namespace {
 const char* const kResponseTypes[] = {"text", "blob", "arraybuffer", "any"};
 
-COMPILE_ASSERT(arraysize(kResponseTypes) ==
-                   cobalt::web::MessageEvent::kResponseTypeMax,
+COMPILE_ASSERT(arraysize(kResponseTypes) == MessageEvent::kResponseTypeMax,
                enum_and_array_size_mismatch);
 
 }  // namespace
 
-namespace cobalt {
-namespace web {
+MessageEvent::MessageEvent(const std::string& type,
+                           const MessageEventInit& init_dict)
+    : Event(type, init_dict), response_type_(kAny) {
+  if (init_dict.has_data() && init_dict.data()) {
+    data_ = script::SerializeScriptValue(*(init_dict.data()));
+  }
+  if (init_dict.has_origin()) {
+    origin_ = init_dict.origin();
+  }
+  if (init_dict.has_last_event_id()) {
+    last_event_id_ = init_dict.last_event_id();
+  }
+  if (init_dict.has_source()) {
+    source_ = init_dict.source();
+  }
+  if (init_dict.has_ports()) {
+    ports_ = init_dict.ports();
+  }
+}
 
 // static
 std::string MessageEvent::GetResponseTypeAsString(
@@ -63,7 +82,7 @@
 MessageEvent::Response MessageEvent::data(
     script::EnvironmentSettings* settings) const {
   if (!data_ && !data_io_buffer_) {
-    return Response("");
+    return Response(script::Handle<script::ValueHandle>());
   }
 
   script::GlobalEnvironment* global_environment = nullptr;
@@ -71,7 +90,7 @@
       response_type_ == kAny) {
     DCHECK(settings);
     global_environment =
-        base::polymorphic_downcast<web::EnvironmentSettings*>(settings)
+        base::polymorphic_downcast<EnvironmentSettings*>(settings)
             ->context()
             ->global_environment();
     DCHECK(global_environment);
@@ -92,8 +111,7 @@
           std::move(buffer_copy);
       if (response_type_ == kBlob) {
         DCHECK(settings);
-        scoped_refptr<web::Blob> blob =
-            new web::Blob(settings, response_buffer);
+        scoped_refptr<Blob> blob = new Blob(settings, response_buffer);
         return Response(blob);
       }
       return Response(response_buffer);
@@ -101,12 +119,14 @@
     case kAny: {
       v8::Isolate* isolate = global_environment->isolate();
       script::v8c::EntryScope entry_scope(isolate);
+      DCHECK(isolate);
+      DCHECK(data_);
       return Response(script::Handle<script::ValueHandle>(
           std::move(script::DeserializeScriptValue(isolate, *data_))));
     }
     default:
       NOTREACHED() << "Invalid response type.";
-      return Response("");
+      return Response(script::Handle<script::ValueHandle>());
   }
 }
 
diff --git a/cobalt/web/message_event.h b/cobalt/web/message_event.h
index b39a198..ba81c1f 100644
--- a/cobalt/web/message_event.h
+++ b/cobalt/web/message_event.h
@@ -20,28 +20,32 @@
 #include <string>
 #include <utility>
 
-#include "base/memory/ref_counted.h"
+#include "base/memory/scoped_refptr.h"
 #include "base/strings/string_piece.h"
 #include "cobalt/base/token.h"
 #include "cobalt/script/array_buffer.h"
+#include "cobalt/script/sequence.h"
 #include "cobalt/script/union_type.h"
-#include "cobalt/script/v8c/v8c_value_handle.h"
-#include "cobalt/script/wrappable.h"
+#include "cobalt/script/value_handle.h"
 #include "cobalt/web/blob.h"
 #include "cobalt/web/event.h"
+#include "cobalt/web/event_target.h"
 #include "cobalt/web/message_event_init.h"
+#include "cobalt/web/message_port.h"
 #include "net/base/io_buffer.h"
 #include "v8/include/v8.h"
 
 namespace cobalt {
 namespace web {
 
-class MessageEvent : public web::Event {
+class MessageEvent : public Event {
  public:
-  typedef script::UnionType4<std::string, scoped_refptr<web::Blob>,
+  typedef script::UnionType4<std::string, scoped_refptr<Blob>,
                              script::Handle<script::ArrayBuffer>,
                              script::Handle<script::ValueHandle>>
       Response;
+
+
   // These response types are ordered in the likelihood of being used.
   // Keeping them in expected order will help make code faster.
   enum ResponseType { kText, kBlob, kArrayBuffer, kAny, kResponseTypeMax };
@@ -56,14 +60,26 @@
   MessageEvent(base::Token type, ResponseType response_type,
                const scoped_refptr<net::IOBufferWithSize>& data)
       : Event(type), response_type_(response_type), data_io_buffer_(data) {}
-  MessageEvent(const std::string& type, const web::MessageEventInit& init_dict)
-      : Event(type, init_dict),
-        response_type_(kAny),
-        data_(script::SerializeScriptValue(*(init_dict.data()))) {}
+  MessageEvent(const std::string& type, const MessageEventInit& init_dict);
 
   Response data(script::EnvironmentSettings* settings = nullptr) const;
+  const std::string& origin() const { return origin_; }
+  const std::string& last_event_id() const { return last_event_id_; }
+  const scoped_refptr<EventTarget>& source() const { return source_; }
+  script::Sequence<scoped_refptr<MessagePort>> ports() const { return ports_; }
 
   // These helper functions are custom, and not in any spec.
+  void set_origin(const std::string& origin) { origin_ = origin; }
+  void set_last_event_id(const std::string& last_event_id) {
+    last_event_id_ = last_event_id;
+  }
+  void set_source(const scoped_refptr<EventTarget>& source) {
+    source_ = source;
+  }
+  void set_ports(script::Sequence<scoped_refptr<MessagePort>> ports) {
+    ports_ = ports;
+  }
+
   static std::string GetResponseTypeAsString(const ResponseType response_type);
   static MessageEvent::ResponseType GetResponseType(base::StringPiece to_match);
 
@@ -73,6 +89,10 @@
   ResponseType response_type_ = kText;
   scoped_refptr<net::IOBufferWithSize> data_io_buffer_;
   std::unique_ptr<script::DataBuffer> data_;
+  std::string origin_;
+  std::string last_event_id_;
+  scoped_refptr<EventTarget> source_;
+  script::Sequence<scoped_refptr<MessagePort>> ports_;
 };
 
 }  // namespace web
diff --git a/cobalt/web/message_event.idl b/cobalt/web/message_event.idl
index 0e15ab7..909fb66 100644
--- a/cobalt/web/message_event.idl
+++ b/cobalt/web/message_event.idl
@@ -20,4 +20,9 @@
   Constructor(DOMString type, optional MessageEventInit eventInitDict)
 ] interface MessageEvent : Event {
   [CallWith = EnvironmentSettings] readonly attribute any data;
+  readonly attribute USVString origin;
+  readonly attribute DOMString lastEventId;
+  readonly attribute EventTarget? source;
+  // TODO(b/236750294): Make this be FrozenArray<MessagePort> when available.
+  readonly attribute sequence<MessagePort> ports;
 };
diff --git a/cobalt/web/message_event_init.idl b/cobalt/web/message_event_init.idl
index 9ccf914..edf7e7c 100644
--- a/cobalt/web/message_event_init.idl
+++ b/cobalt/web/message_event_init.idl
@@ -12,8 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-// https://www.w3.org/TR/html50/webappapis.html#erroreventinit
+// https://html.spec.whatwg.org/multipage/comms.html#messageeventinit
 
 dictionary MessageEventInit : EventInit {
-  any data = null;
+  any data;
+  USVString origin = "";
+  DOMString lastEventId = "";
+  EventTarget? source;
+  sequence<MessagePort> ports;
 };
diff --git a/cobalt/web/message_event_test.cc b/cobalt/web/message_event_test.cc
index d8f6d6d..93893b2 100644
--- a/cobalt/web/message_event_test.cc
+++ b/cobalt/web/message_event_test.cc
@@ -19,11 +19,14 @@
 
 #include "base/bind.h"
 #include "base/logging.h"
-#include "cobalt/dom/testing/test_with_javascript.h"
+#include "base/memory/scoped_refptr.h"
 #include "cobalt/script/v8c/entry_scope.h"
 #include "cobalt/script/value_handle.h"
+#include "cobalt/web/blob.h"
 #include "cobalt/web/message_event_init.h"
 #include "cobalt/web/testing/gtest_workarounds.h"
+#include "cobalt/web/testing/test_with_javascript.h"
+#include "net/base/io_buffer.h"
 #include "testing/gmock/include/gmock/gmock.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
@@ -31,8 +34,7 @@
 namespace web {
 
 namespace {
-class MessageEventTestWithJavaScript : public dom::testing::TestWithJavaScript {
-};
+class MessageEventTestWithJavaScript : public testing::TestWebWithJavaScript {};
 }  // namespace
 
 TEST(MessageEventTest, ConstructorWithEventTypeString) {
@@ -49,19 +51,44 @@
   EXPECT_FALSE(event->propagation_stopped());
   EXPECT_FALSE(event->immediate_propagation_stopped());
   MessageEvent::Response event_data = event->data();
-  EXPECT_TRUE(event_data.IsType<std::string>());
-  EXPECT_EQ("", event_data.AsType<std::string>());
+  EXPECT_TRUE(event_data.IsType<script::Handle<script::ValueHandle>>());
+  EXPECT_TRUE(
+      event_data.AsType<script::Handle<script::ValueHandle>>().IsEmpty());
 }
 
-TEST_F(MessageEventTestWithJavaScript,
-       ConstructorWithEventTypeAndDefaultInitDict) {
-  MessageEventInit init;
-  base::Optional<script::ValueHandleHolder::Reference> reference;
-  EvaluateScript("'data_value'", window(), &reference);
-  init.set_data(&(reference->referenced_value()));
-  scoped_refptr<MessageEvent> event = new MessageEvent("mytestevent", init);
+TEST(MessageEventTest, ConstructorWithText) {
+  std::string message_string("ConstructorWithTextMessageData");
+  scoped_refptr<net::IOBufferWithSize> data =
+      base::MakeRefCounted<net::IOBufferWithSize>(message_string.size());
+  memcpy(data->data(), message_string.c_str(), message_string.size());
+  scoped_refptr<MessageEvent> event =
+      new MessageEvent(base::Tokens::message(), MessageEvent::kText, data);
 
-  EXPECT_EQ("mytestevent", event->type());
+  EXPECT_EQ("message", event->type());
+  EXPECT_EQ(NULL, event->target().get());
+  EXPECT_EQ(NULL, event->current_target().get());
+  EXPECT_EQ(Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+  MessageEvent::Response event_data = event->data();
+  EXPECT_TRUE(event_data.IsType<std::string>());
+  EXPECT_EQ(message_string.size(), event_data.AsType<std::string>().size());
+  EXPECT_EQ("ConstructorWithTextMessageData", event_data.AsType<std::string>());
+}
+
+TEST_P(MessageEventTestWithJavaScript, ConstructorWithBlob) {
+  std::string message_string("ConstructorWithBlobMessageData");
+  scoped_refptr<net::IOBufferWithSize> data =
+      base::MakeRefCounted<net::IOBufferWithSize>(message_string.size());
+  memcpy(data->data(), message_string.c_str(), message_string.size());
+  scoped_refptr<MessageEvent> event =
+      new MessageEvent(base::Tokens::message(), MessageEvent::kBlob, data);
+
+  EXPECT_EQ("message", event->type());
   EXPECT_EQ(NULL, event->target().get());
   EXPECT_EQ(NULL, event->current_target().get());
   EXPECT_EQ(Event::kNone, event->event_phase());
@@ -72,7 +99,139 @@
   EXPECT_FALSE(event->propagation_stopped());
   EXPECT_FALSE(event->immediate_propagation_stopped());
   MessageEvent::Response event_data =
-      event->data(window()->environment_settings());
+      event->data(web_context()->environment_settings());
+  EXPECT_TRUE(event_data.IsType<scoped_refptr<Blob>>());
+  EXPECT_EQ(message_string.size(),
+            event_data.AsType<scoped_refptr<Blob>>()->size());
+  EXPECT_EQ("ConstructorWithBlobMessageData",
+            std::string(reinterpret_cast<const char*>(
+                            event_data.AsType<scoped_refptr<Blob>>()->data()),
+                        static_cast<size_t>(
+                            event_data.AsType<scoped_refptr<Blob>>()->size())));
+  EXPECT_TRUE(event_data.AsType<scoped_refptr<Blob>>()->type().empty());
+}
+
+TEST_P(MessageEventTestWithJavaScript, ConstructorWithArrayBuffer) {
+  std::string message_string("ConstructorWithArrayBufferMessageData");
+  scoped_refptr<net::IOBufferWithSize> data =
+      base::MakeRefCounted<net::IOBufferWithSize>(message_string.size());
+  memcpy(data->data(), message_string.c_str(), message_string.size());
+  scoped_refptr<MessageEvent> event = new MessageEvent(
+      base::Tokens::message(), MessageEvent::kArrayBuffer, data);
+
+  EXPECT_EQ("message", event->type());
+  EXPECT_EQ(NULL, event->target().get());
+  EXPECT_EQ(NULL, event->current_target().get());
+  EXPECT_EQ(Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+  MessageEvent::Response event_data =
+      event->data(web_context()->environment_settings());
+  EXPECT_TRUE(event_data.IsType<script::Handle<script::ArrayBuffer>>());
+  EXPECT_EQ(
+      message_string.size(),
+      event_data.AsType<script::Handle<script::ArrayBuffer>>()->ByteLength());
+  EXPECT_EQ(
+      "ConstructorWithArrayBufferMessageData",
+      std::string(
+          reinterpret_cast<const char*>(
+              event_data.AsType<script::Handle<script::ArrayBuffer>>()->Data()),
+          static_cast<size_t>(
+              event_data.AsType<script::Handle<script::ArrayBuffer>>()
+                  ->ByteLength())));
+}
+
+TEST_P(MessageEventTestWithJavaScript, ConstructorWithAny) {
+  base::Optional<script::ValueHandleHolder::Reference> reference;
+  EvaluateScript("'ConstructorWithAnyMessageData'", &reference);
+  std::unique_ptr<script::DataBuffer> data(
+      script::SerializeScriptValue(reference->referenced_value()));
+  EXPECT_NE(nullptr, data.get());
+  EXPECT_NE(nullptr, data->ptr);
+  EXPECT_GT(data->size, 0U);
+  scoped_refptr<MessageEvent> event =
+      new MessageEvent(base::Tokens::message(), std::move(data));
+
+  EXPECT_EQ("message", event->type());
+  EXPECT_EQ(NULL, event->target().get());
+  EXPECT_EQ(NULL, event->current_target().get());
+  EXPECT_EQ(Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+  MessageEvent::Response event_data =
+      event->data(web_context()->environment_settings());
+  EXPECT_TRUE(event_data.IsType<script::Handle<script::ValueHandle>>());
+  EXPECT_FALSE(
+      event_data.AsType<script::Handle<script::ValueHandle>>().IsEmpty());
+  auto script_value =
+      event_data.AsType<script::Handle<script::ValueHandle>>().GetScriptValue();
+  auto* isolate = script::GetIsolate(*script_value);
+  script::v8c::EntryScope entry_scope(isolate);
+  v8::Local<v8::Value> v8_value = script::GetV8Value(*script_value);
+  std::string actual =
+      *(v8::String::Utf8Value(isolate, v8_value.As<v8::String>()));
+  EXPECT_EQ("ConstructorWithAnyMessageData", actual);
+}
+
+TEST_P(MessageEventTestWithJavaScript,
+       ConstructorWithEventTypeAndDefaultInitDict) {
+  MessageEventInit init;
+  scoped_refptr<MessageEvent> event = new MessageEvent("mytestevent", init);
+
+  EXPECT_EQ("mytestevent", event->type());
+  EXPECT_EQ(nullptr, event->target().get());
+  EXPECT_EQ(nullptr, event->current_target().get());
+  EXPECT_EQ(Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+
+  MessageEvent::Response event_data =
+      event->data(web_context()->environment_settings());
+  EXPECT_TRUE(event_data.IsType<script::Handle<script::ValueHandle>>());
+  EXPECT_TRUE(
+      event_data.AsType<script::Handle<script::ValueHandle>>().IsEmpty());
+
+  EXPECT_TRUE(event->origin().empty());
+  EXPECT_TRUE(event->last_event_id().empty());
+  EXPECT_EQ(nullptr, event->source().get());
+  EXPECT_TRUE(event->ports().empty());
+}
+
+TEST_P(MessageEventTestWithJavaScript, ConstructorWithEventTypeAndInitDict) {
+  MessageEventInit init;
+  base::Optional<script::ValueHandleHolder::Reference> reference;
+  EvaluateScript("'data_value'", &reference);
+  init.set_data(&(reference->referenced_value()));
+  init.set_origin("OriginString");
+  init.set_last_event_id("lastEventIdString");
+  init.set_source(web_context()->GetWindowOrWorkerGlobalScope());
+  scoped_refptr<MessageEvent> event = new MessageEvent("mytestevent", init);
+
+  EXPECT_EQ("mytestevent", event->type());
+  EXPECT_EQ(nullptr, event->target().get());
+  EXPECT_EQ(nullptr, event->current_target().get());
+  EXPECT_EQ(Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+
+  MessageEvent::Response event_data =
+      event->data(web_context()->environment_settings());
   EXPECT_TRUE(event_data.IsType<script::Handle<script::ValueHandle>>());
   auto script_value =
       event_data.AsType<script::Handle<script::ValueHandle>>().GetScriptValue();
@@ -82,24 +241,44 @@
   std::string actual =
       *(v8::String::Utf8Value(isolate, v8_value.As<v8::String>()));
   EXPECT_EQ("data_value", actual);
+
+  EXPECT_EQ("OriginString", event->origin());
+  EXPECT_EQ("lastEventIdString", event->last_event_id());
+  EXPECT_EQ(web_context()->GetWindowOrWorkerGlobalScope(),
+            event->source().get());
+  EXPECT_TRUE(event->ports().empty());
 }
 
-TEST_F(MessageEventTestWithJavaScript,
-       ConstructorWithEventTypeAndErrorInitDict) {
+TEST_P(MessageEventTestWithJavaScript,
+       ConstructorWithEventTypeAndMessageEventInitDict) {
   std::string result;
   EXPECT_TRUE(
       EvaluateScript("var event = new MessageEvent('dog', "
-                     "    {'cancelable':true, "
-                     "     'data':'data_value'});"
+                     "    {cancelable: true, "
+                     "     origin: 'OriginValue',"
+                     "     lastEventId: 'LastEventIdValue',"
+                     "     source: this,"
+                     "     data: {value: 'data_value'},"
+                     "    }"
+                     ");"
                      "if (event.type == 'dog' &&"
                      "    event.bubbles == false &&"
                      "    event.cancelable == true &&"
-                     "    event.data == 'data_value') "
-                     "    event.data;",
+                     "    event.origin == 'OriginValue' &&"
+                     "    event.lastEventId == 'LastEventIdValue' &&"
+                     "    event.source == this &&"
+                     "    event.ports.length == 0 &&"
+                     "    event.data.value == 'data_value') "
+                     "    event.data.value;",
                      &result))
       << "Failed to evaluate script.";
   EXPECT_EQ("data_value", result);
 }
 
+INSTANTIATE_TEST_CASE_P(
+    MessageEventTestsWithJavaScript, MessageEventTestWithJavaScript,
+    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWebTypes()),
+    testing::TestWebWithJavaScript::GetTypeName);
+
 }  // namespace web
 }  // namespace cobalt
diff --git a/cobalt/web/message_port.h b/cobalt/web/message_port.h
index 183e35a..68ae01e 100644
--- a/cobalt/web/message_port.h
+++ b/cobalt/web/message_port.h
@@ -29,7 +29,6 @@
 #include "cobalt/web/context.h"
 #include "cobalt/web/event_target.h"
 #include "cobalt/web/event_target_listener_info.h"
-#include "cobalt/web/message_event.h"
 
 namespace cobalt {
 namespace web {
@@ -91,6 +90,8 @@
                                              event_listener);
   }
 
+  web::EventTarget* event_target() { return event_target_; }
+
   DEFINE_WRAPPABLE_TYPE(MessagePort);
 
  private:
diff --git a/cobalt/web/message_port.idl b/cobalt/web/message_port.idl
index 86b9528..39a4f6e 100644
--- a/cobalt/web/message_port.idl
+++ b/cobalt/web/message_port.idl
@@ -14,10 +14,9 @@
 
 // https://html.spec.whatwg.org/multipage/web-messaging.html#messageport
 // https://html.spec.whatwg.org/dev/web-messaging.html#message-ports
-interface MessagePort /* : EventTarget */ {
+[Exposed = (Window,Worker)] interface MessagePort /* : EventTarget */ {
   // [RaisesException]
   void postMessage(any message);
-  // void postMessage(USVString message, sequence<USVString> transfer);
   // TODO: Support sequence<object>: b/218501774
   // void postMessage(any message, sequence<object> transfer);
   // TODO: Support overloads with dictionary parameter: b/218506730
@@ -29,4 +28,4 @@
   // event handlers
   attribute EventHandler onmessage;
   attribute EventHandler onmessageerror;
-  };
+};
diff --git a/cobalt/web/multi_cache_query_options.idl b/cobalt/web/multi_cache_query_options.idl
new file mode 100644
index 0000000..5ad9e71
--- /dev/null
+++ b/cobalt/web/multi_cache_query_options.idl
@@ -0,0 +1,19 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// https://w3c.github.io/ServiceWorker/#dictdef-multicachequeryoptions
+
+dictionary MultiCacheQueryOptions {
+  DOMString cacheName;
+};
diff --git a/cobalt/web/testing/stub_web_context.h b/cobalt/web/testing/stub_web_context.h
index dabc4b6..dd97ea3 100644
--- a/cobalt/web/testing/stub_web_context.h
+++ b/cobalt/web/testing/stub_web_context.h
@@ -66,8 +66,7 @@
   // WebInstance
   //
   base::MessageLoop* message_loop() const final {
-    NOTREACHED();
-    return nullptr;
+    return base::MessageLoop::current();
   }
   void ShutDownJavaScriptEngine() final { NOTREACHED(); }
   loader::FetcherFactory* fetcher_factory() const final {
diff --git a/cobalt/web/testing/test_with_javascript.h b/cobalt/web/testing/test_with_javascript.h
index f37276b..011ab7a 100644
--- a/cobalt/web/testing/test_with_javascript.h
+++ b/cobalt/web/testing/test_with_javascript.h
@@ -23,7 +23,6 @@
 #include "cobalt/dom/testing/stub_window.h"
 #include "cobalt/dom/window.h"
 #include "cobalt/script/wrappable.h"
-#include "cobalt/web/window_or_worker_global_scope.h"
 #include "cobalt/worker/testing/test_with_javascript.h"
 
 namespace cobalt {
@@ -42,17 +41,14 @@
       DCHECK(!this->worker_global_scope());
       this->ClearWebContext();
       window_.reset(new dom::testing::StubWindow());
+      window_->InitializeWindow();
     }
   }
 
-  WindowOrWorkerGlobalScope* window_or_worker_global_scope() {
-    return window_ ? window_->window().get() : this->worker_global_scope();
-  }
-
-  scoped_refptr<script::GlobalEnvironment> global_environment() override {
-    if (window_) return window_->global_environment();
+  web::Context* web_context() const override {
+    if (window_) return window_->web_context();
     return worker::testing::TestWithJavaScriptBase<
-        TypeIdProvider>::global_environment();
+        TypeIdProvider>::web_context();
   }
 
  private:
@@ -66,7 +62,7 @@
  public:
   // Return a vector of values for all known worker types, to be used in the
   // INSTANTIATE_TEST_CASE_P() declaration.
-  static std::vector<base::TypeId> GetWorkerTypes() {
+  static std::vector<base::TypeId> GetWebTypes() {
     std::vector<base::TypeId> worker_types =
         worker::testing::TestWorkersWithJavaScript::GetWorkerTypes();
     worker_types.push_back(base::GetTypeId<dom::Window>());
diff --git a/cobalt/web/url_test.cc b/cobalt/web/url_test.cc
index ee6f405..fa0dbcc 100644
--- a/cobalt/web/url_test.cc
+++ b/cobalt/web/url_test.cc
@@ -195,7 +195,7 @@
 
 INSTANTIATE_TEST_CASE_P(
     URLTestsWithJavaScript, URLTestWithJavaScript,
-    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWorkerTypes()),
+    ::testing::ValuesIn(testing::TestWebWithJavaScript::GetWebTypes()),
     testing::TestWebWithJavaScript::GetTypeName);
 
 
diff --git a/cobalt/web/window_or_worker_global_scope.cc b/cobalt/web/window_or_worker_global_scope.cc
index f5a3fc3..769cd77 100644
--- a/cobalt/web/window_or_worker_global_scope.cc
+++ b/cobalt/web/window_or_worker_global_scope.cc
@@ -35,6 +35,7 @@
     // Global scope object EventTargets require special handling for onerror
     // events, see EventTarget constructor for more details.
     : EventTarget(settings, kUnpackOnErrorEvents),
+      caches_(new CacheStorage()),
       crypto_(new Crypto()),
       window_timers_(this, stat_tracker, debugger_hooks(),
                      options.initial_state),
@@ -87,6 +88,10 @@
   window_timers_.DisableCallbacks();
 }
 
+scoped_refptr<CacheStorage> WindowOrWorkerGlobalScope::caches() const {
+  return caches_;
+}
+
 scoped_refptr<Crypto> WindowOrWorkerGlobalScope::crypto() const {
   return crypto_;
 }
diff --git a/cobalt/web/window_or_worker_global_scope.h b/cobalt/web/window_or_worker_global_scope.h
index b2c8ea4..cbb74dc 100644
--- a/cobalt/web/window_or_worker_global_scope.h
+++ b/cobalt/web/window_or_worker_global_scope.h
@@ -23,6 +23,7 @@
 #include "base/memory/ref_counted.h"
 #include "cobalt/loader/cors_preflight_cache.h"
 #include "cobalt/script/environment_settings.h"
+#include "cobalt/web/cache_storage.h"
 #include "cobalt/web/crypto.h"
 #include "cobalt/web/csp_delegate.h"
 #include "cobalt/web/csp_delegate_type.h"
@@ -98,7 +99,10 @@
     return nullptr;
   }
 
-  web::CspDelegate* csp_delegate() const { return csp_delegate_.get(); }
+  web::CspDelegate* csp_delegate() const {
+    DCHECK(csp_delegate_);
+    return csp_delegate_.get();
+  }
 
   const scoped_refptr<loader::CORSPreflightCache> get_preflight_cache() {
     return preflight_cache_;
@@ -129,6 +133,10 @@
 
   void DestroyTimers();
 
+  // Web API: GlobalCacheStorage (implements)
+  //   https://w3c.github.io/ServiceWorker/#cachestorage
+  scoped_refptr<CacheStorage> caches() const;
+
   // Web API: GlobalCrypto (implements)
   //   https://www.w3.org/TR/WebCryptoAPI/#crypto-interface
   scoped_refptr<Crypto> crypto() const;
@@ -136,6 +144,7 @@
  protected:
   virtual ~WindowOrWorkerGlobalScope() {}
 
+  scoped_refptr<CacheStorage> caches_;
   scoped_refptr<Crypto> crypto_;
   web::WindowTimers window_timers_;
 
diff --git a/cobalt/webdriver/screenshot.cc b/cobalt/webdriver/screenshot.cc
index 9c7373b..184e9c0 100644
--- a/cobalt/webdriver/screenshot.cc
+++ b/cobalt/webdriver/screenshot.cc
@@ -1,3 +1,4 @@
+
 // Copyright 2019 The Cobalt Authors. All Rights Reserved.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/cobalt/websocket/web_socket.cc b/cobalt/websocket/web_socket.cc
index 82db7a7..e842657 100644
--- a/cobalt/websocket/web_socket.cc
+++ b/cobalt/websocket/web_socket.cc
@@ -562,8 +562,6 @@
 }
 
 web::CspDelegate* WebSocket::csp_delegate() const {
-  DCHECK(environment_settings());
-  DCHECK(environment_settings()->context());
   DCHECK(environment_settings()->context()->GetWindowOrWorkerGlobalScope());
   return environment_settings()
       ->context()
diff --git a/cobalt/worker/BUILD.gn b/cobalt/worker/BUILD.gn
index 8c0077a..cf05426 100644
--- a/cobalt/worker/BUILD.gn
+++ b/cobalt/worker/BUILD.gn
@@ -26,7 +26,9 @@
     "dedicated_worker.h",
     "dedicated_worker_global_scope.cc",
     "dedicated_worker_global_scope.h",
+    "extendable_event.cc",
     "extendable_event.h",
+    "extendable_message_event.cc",
     "extendable_message_event.h",
     "navigation_preload_manager.cc",
     "navigation_preload_manager.h",
@@ -80,6 +82,7 @@
 
   sources = [
     "dedicated_worker_global_scope_test.cc",
+    "extendable_message_event_test.cc",
     "service_worker_global_scope_test.cc",
     "worker_global_scope_test.cc",
     "worker_location_test.cc",
diff --git a/cobalt/worker/client.h b/cobalt/worker/client.h
index c40ca72..53a5f8f 100644
--- a/cobalt/worker/client.h
+++ b/cobalt/worker/client.h
@@ -29,7 +29,7 @@
 class Client : public web::MessagePort {
  public:
   // https://w3c.github.io/ServiceWorker/#create-client-algorithm
-  static Client* Create(web::EnvironmentSettings* client) {
+  static scoped_refptr<Client> Create(web::EnvironmentSettings* client) {
     return new Client(client);
   }
   ~Client() { service_worker_client_ = nullptr; }
diff --git a/cobalt/worker/extendable_event.cc b/cobalt/worker/extendable_event.cc
new file mode 100644
index 0000000..6b49b47
--- /dev/null
+++ b/cobalt/worker/extendable_event.cc
@@ -0,0 +1,83 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/worker/extendable_event.h"
+
+namespace cobalt {
+namespace worker {
+
+void ExtendableEvent::WaitUntil(
+    script::EnvironmentSettings* settings,
+    std::unique_ptr<script::Promise<script::ValueHandle*>>& promise,
+    script::ExceptionState* exception_state) {
+  // Algorithm for waitUntil(), to add lifetime promise to event.
+  //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+
+  // 1. If event’s isTrusted attribute is false, throw an "InvalidStateError"
+  //    DOMException.
+  // 2. If event is not active, throw an "InvalidStateError" DOMException.
+  if (!IsActive()) {
+    web::DOMException::Raise(web::DOMException::kInvalidStateErr,
+                             exception_state);
+    return;
+  }
+  // 3. Add promise to event’s extend lifetime promises.
+  // 4. Increment event’s pending promises count by one.
+  ++pending_promise_count_;
+  // 5. Upon fulfillment or rejection of promise, queue a microtask to run
+  //    these substeps:
+  std::unique_ptr<base::OnceCallback<void()>> callback(
+      new base::OnceCallback<void()>(std::move(
+          base::BindOnce(&ExtendableEvent::StateChange, base::Unretained(this),
+                         settings, promise.get()))));
+  promise->AddStateChangeCallback(std::move(callback));
+  promise.release();
+}
+
+void ExtendableEvent::StateChange(
+    script::EnvironmentSettings* settings,
+    const script::Promise<script::ValueHandle*>* promise) {
+  // Implement the microtask called upon fulfillment or rejection of a
+  // promise, as part of the algorithm for waitUntil().
+  //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
+  DCHECK(promise);
+  has_rejected_promise_ |= promise->State() == script::PromiseState::kRejected;
+  // 5.1. Decrement event’s pending promises count by one.
+  --pending_promise_count_;
+  // 5.2. If event’s pending promises count is 0, then:
+  if (0 == pending_promise_count_) {
+    if (done_callback_) {
+      std::move(done_callback_).Run(has_rejected_promise_);
+    }
+    web::Context* context =
+        base::polymorphic_downcast<web::EnvironmentSettings*>(settings)
+            ->context();
+    ServiceWorkerJobs* jobs = context->service_worker_jobs();
+    DCHECK(jobs);
+    // 5.2.1. Let registration be the current global object's associated
+    //        service worker's containing service worker registration.
+    jobs->message_loop()->task_runner()->PostTask(
+        FROM_HERE,
+        base::BindOnce(
+            &ServiceWorkerJobs::WaitUntilSubSteps, base::Unretained(jobs),
+            base::Unretained(context->GetWindowOrWorkerGlobalScope()
+                                 ->AsServiceWorker()
+                                 ->service_worker_object()
+                                 ->containing_service_worker_registration())));
+  }
+  delete promise;
+}
+
+}  // namespace worker
+}  // namespace cobalt
diff --git a/cobalt/worker/extendable_event.h b/cobalt/worker/extendable_event.h
index e7d7a6a..949c7b1 100644
--- a/cobalt/worker/extendable_event.h
+++ b/cobalt/worker/extendable_event.h
@@ -44,75 +44,22 @@
 class ExtendableEvent : public web::Event {
  public:
   explicit ExtendableEvent(const std::string& type) : Event(type) {}
-  explicit ExtendableEvent(base::Token type,
-                           base::OnceCallback<void(bool)> done_callback =
-                               base::OnceCallback<void(bool)>())
-      : Event(type), done_callback_(std::move(done_callback)) {}
   ExtendableEvent(const std::string& type, const ExtendableEventInit& init_dict)
+      : ExtendableEvent(base::Token(type), init_dict) {}
+  ExtendableEvent(base::Token type,
+                  base::OnceCallback<void(bool)> done_callback =
+                      base::OnceCallback<void(bool)>())
+      : Event(type), done_callback_(std::move(done_callback)) {}
+  ExtendableEvent(base::Token type, const ExtendableEventInit& init_dict)
       : Event(type, init_dict) {}
 
   void WaitUntil(
       script::EnvironmentSettings* settings,
       std::unique_ptr<script::Promise<script::ValueHandle*>>& promise,
-      script::ExceptionState* exception_state) {
-    // Algorithm for waitUntil(), to add lifetime promise to event.
-    //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
-
-    // 1. If event’s isTrusted attribute is false, throw an "InvalidStateError"
-    //    DOMException.
-    // 2. If event is not active, throw an "InvalidStateError" DOMException.
-    if (!IsActive()) {
-      web::DOMException::Raise(web::DOMException::kInvalidStateErr,
-                               exception_state);
-      return;
-    }
-    // 3. Add promise to event’s extend lifetime promises.
-    // 4. Increment event’s pending promises count by one.
-    ++pending_promise_count_;
-    // 5. Upon fulfillment or rejection of promise, queue a microtask to run
-    //    these substeps:
-    std::unique_ptr<base::OnceCallback<void()>> callback(
-        new base::OnceCallback<void()>(std::move(
-            base::BindOnce(&ExtendableEvent::StateChange,
-                           base::Unretained(this), settings, promise.get()))));
-    promise->AddStateChangeCallback(std::move(callback));
-    promise.release();
-  }
+      script::ExceptionState* exception_state);
 
   void StateChange(script::EnvironmentSettings* settings,
-                   const script::Promise<script::ValueHandle*>* promise) {
-    // Implement the microtask called upon fulfillment or rejection of a
-    // promise, as part of the algorithm for waitUntil().
-    //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
-    DCHECK(promise);
-    has_rejected_promise_ |=
-        promise->State() == script::PromiseState::kRejected;
-    // 5.1. Decrement event’s pending promises count by one.
-    --pending_promise_count_;
-    // 5.2. If event’s pending promises count is 0, then:
-    if (0 == pending_promise_count_) {
-      if (done_callback_) {
-        std::move(done_callback_).Run(has_rejected_promise_);
-      }
-      web::Context* context =
-          base::polymorphic_downcast<web::EnvironmentSettings*>(settings)
-              ->context();
-      ServiceWorkerJobs* jobs = context->service_worker_jobs();
-      DCHECK(jobs);
-      // 5.2.1. Let registration be the current global object's associated
-      //        service worker's containing service worker registration.
-      jobs->message_loop()->task_runner()->PostTask(
-          FROM_HERE,
-          base::BindOnce(&ServiceWorkerJobs::WaitUntilSubSteps,
-                         base::Unretained(jobs),
-                         base::Unretained(
-                             context->GetWindowOrWorkerGlobalScope()
-                                 ->AsServiceWorker()
-                                 ->service_worker_object()
-                                 ->containing_service_worker_registration())));
-    }
-    delete promise;
-  }
+                   const script::Promise<script::ValueHandle*>* promise);
 
   bool IsActive() {
     // An ExtendableEvent object is said to be active when its timed out flag
diff --git a/cobalt/worker/extendable_event_init.idl b/cobalt/worker/extendable_event_init.idl
index d040bfb..5a8c5ed 100644
--- a/cobalt/worker/extendable_event_init.idl
+++ b/cobalt/worker/extendable_event_init.idl
@@ -14,6 +14,6 @@
 
 // https://w3c.github.io/ServiceWorker/#dictdef-extendableeventinit
 
-dictionary ExtendableEventInit : EventInit{
+dictionary ExtendableEventInit : EventInit {
   // Defined for the forward compatibility across the derived events
 };
diff --git a/cobalt/worker/extendable_message_event.cc b/cobalt/worker/extendable_message_event.cc
new file mode 100644
index 0000000..aca29cb
--- /dev/null
+++ b/cobalt/worker/extendable_message_event.cc
@@ -0,0 +1,68 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/worker/extendable_message_event.h"
+
+#include <string>
+#include <utility>
+
+#include "cobalt/script/environment_settings.h"
+#include "cobalt/script/global_environment.h"
+#include "cobalt/script/v8c/entry_scope.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/worker/extendable_message_event_init.h"
+#include "v8/include/v8.h"
+
+namespace cobalt {
+namespace worker {
+
+ExtendableMessageEvent::ExtendableMessageEvent(
+    base::Token type, const ExtendableMessageEventInit& init_dict)
+    : ExtendableEvent(type, init_dict) {
+  if (init_dict.has_data() && init_dict.data()) {
+    DCHECK(init_dict.data());
+    data_ = script::SerializeScriptValue(*(init_dict.data()));
+  }
+  if (init_dict.has_origin()) {
+    origin_ = init_dict.origin();
+  }
+  if (init_dict.has_last_event_id()) {
+    last_event_id_ = init_dict.last_event_id();
+  }
+  if (init_dict.has_source() && init_dict.source().has_value()) {
+    source_ = init_dict.source().value();
+  }
+  if (init_dict.has_ports()) {
+    ports_ = init_dict.ports();
+  }
+}
+
+script::Handle<script::ValueHandle> ExtendableMessageEvent::data(
+    script::EnvironmentSettings* settings) const {
+  if (!settings) return script::Handle<script::ValueHandle>();
+  script::GlobalEnvironment* global_environment =
+      base::polymorphic_downcast<web::EnvironmentSettings*>(settings)
+          ->context()
+          ->global_environment();
+  DCHECK(global_environment);
+  v8::Isolate* isolate = global_environment->isolate();
+  script::v8c::EntryScope entry_scope(isolate);
+  DCHECK(isolate);
+  if (!data_) return script::Handle<script::ValueHandle>();
+  return script::Handle<script::ValueHandle>(
+      std::move(script::DeserializeScriptValue(isolate, *data_)));
+}
+
+}  // namespace worker
+}  // namespace cobalt
diff --git a/cobalt/worker/extendable_message_event.h b/cobalt/worker/extendable_message_event.h
index c6364a7..965f79e 100644
--- a/cobalt/worker/extendable_message_event.h
+++ b/cobalt/worker/extendable_message_event.h
@@ -17,57 +17,75 @@
 
 #include <memory>
 #include <string>
+#include <utility>
 
+#include "base/memory/scoped_refptr.h"
 #include "base/optional.h"
 #include "cobalt/base/token.h"
+#include "cobalt/script/union_type.h"
 #include "cobalt/script/value_handle.h"
 #include "cobalt/script/wrappable.h"
+#include "cobalt/web/event_target.h"
 #include "cobalt/web/message_port.h"
+#include "cobalt/worker/client.h"
 #include "cobalt/worker/extendable_event.h"
 #include "cobalt/worker/extendable_message_event_init.h"
+#include "cobalt/worker/service_worker.h"
 
 namespace cobalt {
 namespace worker {
 
 class ExtendableMessageEvent : public ExtendableEvent {
  public:
-  typedef scoped_refptr<script::Wrappable> SourceType;
+  using SourceType =
+      cobalt::script::UnionType3<scoped_refptr<Client>,
+                                 scoped_refptr<ServiceWorker>,
+                                 scoped_refptr<web::MessagePort>>;
 
   explicit ExtendableMessageEvent(const std::string& type)
       : ExtendableEvent(type) {}
   explicit ExtendableMessageEvent(base::Token type) : ExtendableEvent(type) {}
   ExtendableMessageEvent(const std::string& type,
                          const ExtendableMessageEventInit& init_dict)
-      : ExtendableEvent(type, init_dict) {}
-
-
-  const script::ValueHandleHolder* data() const {
-    if (!data_) {
-      return NULL;
-    }
-
-    return &(data_->referenced_value());
+      : ExtendableMessageEvent(base::Token(type), init_dict) {}
+  ExtendableMessageEvent(base::Token type,
+                         const ExtendableMessageEventInit& init_dict);
+  ExtendableMessageEvent(base::Token type,
+                         const ExtendableMessageEventInit& init_dict,
+                         std::unique_ptr<script::DataBuffer> data)
+      : ExtendableMessageEvent(type, init_dict) {
+    data_ = std::move(data);
   }
 
-  std::string origin() const { return origin_; }
-  std::string last_event_id() const { return last_event_id_; }
+  script::Handle<script::ValueHandle> data(
+      script::EnvironmentSettings* settings = nullptr) const;
 
-  base::Optional<SourceType> source() { return source_; }
-
+  const std::string& origin() const { return origin_; }
+  const std::string& last_event_id() const { return last_event_id_; }
+  SourceType source() const { return source_; }
   script::Sequence<scoped_refptr<MessagePort>> ports() const { return ports_; }
 
+  // These helper functions are custom, and not in any spec.
+  void set_origin(const std::string& origin) { origin_ = origin; }
+  void set_last_event_id(const std::string& last_event_id) {
+    last_event_id_ = last_event_id;
+  }
+  void set_source(const SourceType& source) { source_ = source; }
+  void set_ports(script::Sequence<scoped_refptr<MessagePort>> ports) {
+    ports_ = ports;
+  }
+
   DEFINE_WRAPPABLE_TYPE(ExtendableMessageEvent);
 
  protected:
   ~ExtendableMessageEvent() override {}
 
  private:
-  std::unique_ptr<script::ValueHandleHolder::Reference> data_;
-
-  std::string origin_ = "Origin Stub Value";
-  std::string last_event_id_ = "Last Event Id Stub Value";
-  base::Optional<SourceType> source_;
+  std::string origin_;
+  std::string last_event_id_;
+  SourceType source_;
   script::Sequence<scoped_refptr<MessagePort>> ports_;
+  std::unique_ptr<script::DataBuffer> data_;
 };
 
 }  // namespace worker
diff --git a/cobalt/worker/extendable_message_event.idl b/cobalt/worker/extendable_message_event.idl
index 1e2d5da..30a2001 100644
--- a/cobalt/worker/extendable_message_event.idl
+++ b/cobalt/worker/extendable_message_event.idl
@@ -18,10 +18,10 @@
   Exposed = ServiceWorker,
   Constructor(DOMString type, optional ExtendableMessageEventInit eventInitDict)
 ] interface ExtendableMessageEvent : ExtendableEvent {
-  readonly attribute any data;
+  [CallWith = EnvironmentSettings] readonly attribute any data;
   readonly attribute USVString origin;
   readonly attribute DOMString lastEventId;
-  [SameObject] readonly attribute (Client or ServiceWorker or MessagePort)? source;
+  readonly attribute (Client or ServiceWorker or MessagePort)? source;
   // TODO(b/236750294): Make this be FrozenArray<MessagePort> when available.
   readonly attribute sequence<MessagePort> ports;
 };
diff --git a/cobalt/worker/extendable_message_event_init.idl b/cobalt/worker/extendable_message_event_init.idl
index 25da4cb..7b6cc75 100644
--- a/cobalt/worker/extendable_message_event_init.idl
+++ b/cobalt/worker/extendable_message_event_init.idl
@@ -15,9 +15,9 @@
 // https://w3c.github.io/ServiceWorker/#dictdef-extendablemessageeventinit
 
 dictionary ExtendableMessageEventInit : ExtendableEventInit {
-  any data = null;
+  any data;
   USVString origin = "";
   DOMString lastEventId = "";
-  (Client or ServiceWorker or MessagePort)? source = null;
+  (Client or ServiceWorker or MessagePort) ? source;
   sequence<MessagePort> ports;
 };
diff --git a/cobalt/worker/extendable_message_event_test.cc b/cobalt/worker/extendable_message_event_test.cc
new file mode 100644
index 0000000..2e8e64f
--- /dev/null
+++ b/cobalt/worker/extendable_message_event_test.cc
@@ -0,0 +1,223 @@
+// Copyright 2022 The Cobalt Authors. All Rights Reserved.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "cobalt/worker/extendable_message_event.h"
+
+#include <memory>
+#include <string>
+
+#include "base/bind.h"
+#include "base/logging.h"
+#include "base/memory/scoped_refptr.h"
+#include "cobalt/script/testing/fake_script_value.h"
+#include "cobalt/script/v8c/entry_scope.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/web/testing/gtest_workarounds.h"
+#include "cobalt/web/testing/mock_event_listener.h"
+#include "cobalt/worker/extendable_message_event_init.h"
+#include "cobalt/worker/testing/test_with_javascript.h"
+#include "testing/gmock/include/gmock/gmock.h"
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace cobalt {
+namespace worker {
+
+using script::testing::FakeScriptValue;
+using ::testing::IsSubstring;
+using web::testing::MockEventListener;
+
+namespace {
+class ExtendableMessageEventTestWithJavaScript
+    : public testing::TestServiceWorkerWithJavaScript {
+ protected:
+  ExtendableMessageEventTestWithJavaScript() {
+    fake_event_listener_ = MockEventListener::Create();
+  }
+
+  ~ExtendableMessageEventTestWithJavaScript() override {}
+
+  std::unique_ptr<MockEventListener> fake_event_listener_;
+};
+}  // namespace
+
+TEST_F(ExtendableMessageEventTestWithJavaScript,
+       ConstructorWithEventTypeString) {
+  scoped_refptr<ExtendableMessageEvent> event =
+      new ExtendableMessageEvent("mytestevent");
+
+  EXPECT_EQ("mytestevent", event->type());
+  EXPECT_EQ(NULL, event->target().get());
+  EXPECT_EQ(NULL, event->current_target().get());
+  EXPECT_EQ(web::Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+  script::Handle<script::ValueHandle> event_data =
+      event->data(web_context()->environment_settings());
+  EXPECT_TRUE(event_data.IsEmpty());
+}
+
+TEST_F(ExtendableMessageEventTestWithJavaScript, ConstructorWithAny) {
+  base::Optional<script::ValueHandleHolder::Reference> reference;
+  EvaluateScript("'ConstructorWithAnyMessageData'", &reference);
+  std::unique_ptr<script::DataBuffer> data(
+      script::SerializeScriptValue(reference->referenced_value()));
+  EXPECT_NE(nullptr, data.get());
+  EXPECT_NE(nullptr, data->ptr);
+  EXPECT_GT(data->size, 0U);
+  const ExtendableMessageEventInit init;
+  scoped_refptr<ExtendableMessageEvent> event = new ExtendableMessageEvent(
+      base::Tokens::message(), init, std::move(data));
+
+  EXPECT_EQ("message", event->type());
+  EXPECT_EQ(NULL, event->target().get());
+  EXPECT_EQ(NULL, event->current_target().get());
+  EXPECT_EQ(web::Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+  script::Handle<script::ValueHandle> event_data =
+      event->data(web_context()->environment_settings());
+  EXPECT_FALSE(event_data.IsEmpty());
+  auto script_value = event_data.GetScriptValue();
+  auto* isolate = script::GetIsolate(*script_value);
+  script::v8c::EntryScope entry_scope(isolate);
+  v8::Local<v8::Value> v8_value = script::GetV8Value(*script_value);
+  std::string actual =
+      *(v8::String::Utf8Value(isolate, v8_value.As<v8::String>()));
+  EXPECT_EQ("ConstructorWithAnyMessageData", actual);
+}
+
+TEST_F(ExtendableMessageEventTestWithJavaScript,
+       ConstructorWithEventTypeAndDefaultInitDict) {
+  ExtendableMessageEventInit init;
+  scoped_refptr<ExtendableMessageEvent> event =
+      new ExtendableMessageEvent("mytestevent", init);
+
+  EXPECT_EQ("mytestevent", event->type());
+  EXPECT_EQ(nullptr, event->target().get());
+  EXPECT_EQ(nullptr, event->current_target().get());
+  EXPECT_EQ(web::Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+
+  script::Handle<script::ValueHandle> event_data =
+      event->data(web_context()->environment_settings());
+  EXPECT_TRUE(event_data.IsEmpty());
+  EXPECT_TRUE(event->origin().empty());
+  EXPECT_TRUE(event->last_event_id().empty());
+  EXPECT_FALSE(event->source().IsType<scoped_refptr<Client>>());
+  EXPECT_FALSE(event->source().IsType<scoped_refptr<ServiceWorker>>());
+  EXPECT_FALSE(event->source().IsType<scoped_refptr<web::MessagePort>>());
+  EXPECT_TRUE(event->ports().empty());
+}
+
+TEST_F(ExtendableMessageEventTestWithJavaScript,
+       ConstructorWithEventTypeAndInitDict) {
+  ExtendableMessageEventInit init;
+  base::Optional<script::ValueHandleHolder::Reference> reference;
+  EvaluateScript("'data_value'", &reference);
+  init.set_data(&(reference->referenced_value()));
+  init.set_origin("OriginString");
+  init.set_last_event_id("lastEventIdString");
+  base::Optional<ExtendableMessageEvent::SourceType> client(
+      Client::Create(web_context()->environment_settings()));
+  init.set_source(client);
+  scoped_refptr<ExtendableMessageEvent> event =
+      new ExtendableMessageEvent("mytestevent", init);
+
+  EXPECT_EQ("mytestevent", event->type());
+  EXPECT_EQ(nullptr, event->target().get());
+  EXPECT_EQ(nullptr, event->current_target().get());
+  EXPECT_EQ(web::Event::kNone, event->event_phase());
+  EXPECT_FALSE(event->bubbles());
+  EXPECT_FALSE(event->cancelable());
+  EXPECT_FALSE(event->default_prevented());
+  EXPECT_FALSE(event->IsBeingDispatched());
+  EXPECT_FALSE(event->propagation_stopped());
+  EXPECT_FALSE(event->immediate_propagation_stopped());
+
+  script::Handle<script::ValueHandle> event_data =
+      event->data(web_context()->environment_settings());
+  auto script_value = event_data.GetScriptValue();
+  auto* isolate = script::GetIsolate(*script_value);
+  script::v8c::EntryScope entry_scope(isolate);
+  v8::Local<v8::Value> v8_value = script::GetV8Value(*script_value);
+  std::string actual =
+      *(v8::String::Utf8Value(isolate, v8_value.As<v8::String>()));
+  EXPECT_EQ("data_value", actual);
+
+  EXPECT_EQ("OriginString", event->origin());
+  EXPECT_EQ("lastEventIdString", event->last_event_id());
+  EXPECT_TRUE(event->source().IsType<scoped_refptr<Client>>());
+  EXPECT_EQ(client.value().AsType<scoped_refptr<Client>>().get(),
+            event->source().AsType<scoped_refptr<Client>>().get());
+  EXPECT_TRUE(event->ports().empty());
+}
+
+TEST_F(ExtendableMessageEventTestWithJavaScript,
+       ConstructorWithEventTypeAndInvalidSource) {
+  std::string result;
+  EXPECT_FALSE(
+      EvaluateScript("var event = new ExtendableMessageEvent('dog', "
+                     "    {source: this});"
+                     "if (event.type == 'dog') event.source;",
+                     &result))
+      << "Failed to evaluate script.";
+  EXPECT_PRED_FORMAT2(IsSubstring,
+                      "TypeError: Value is not a member of the union type.",
+                      result);
+  // EXPECT_FALSE(IsSubstring(
+  //    "", "", "TypeError: Value is not a member of the union type.", result));
+}
+
+
+TEST_F(ExtendableMessageEventTestWithJavaScript,
+       ConstructorWithEventTypeAndExtendableMessageEventInitDict) {
+  std::string result;
+  // Note: None of the types for 'source' are constructible, so that can
+  // not easily be tested here.
+  EXPECT_TRUE(
+      EvaluateScript("var event = new ExtendableMessageEvent('dog', "
+                     "    {cancelable: true, "
+                     "     origin: 'OriginValue',"
+                     "     lastEventId: 'LastEventIdValue',"
+                     "     data: {value: 'data_value'},"
+                     "    }"
+                     ");"
+                     "if (event.type == 'dog' &&"
+                     "    event.bubbles == false &&"
+                     "    event.cancelable == true &&"
+                     "    event.origin == 'OriginValue' &&"
+                     "    event.lastEventId == 'LastEventIdValue' &&"
+                     "    event.ports.length == 0 &&"
+                     "    event.data.value == 'data_value') "
+                     "    event.data.value;",
+                     &result))
+      << "Failed to evaluate script.";
+  EXPECT_EQ("data_value", result);
+}
+
+}  // namespace worker
+}  // namespace cobalt
diff --git a/cobalt/worker/navigation_preload_manager.idl b/cobalt/worker/navigation_preload_manager.idl
index 5a05303..92b8175 100644
--- a/cobalt/worker/navigation_preload_manager.idl
+++ b/cobalt/worker/navigation_preload_manager.idl
@@ -14,7 +14,7 @@
 
 // https://w3c.github.io/ServiceWorker/#navigation-preload-manager
 
-[Exposed = (Window, ServiceWorker)] interface NavigationPreloadManager {
+[Exposed = (Window,ServiceWorker)] interface NavigationPreloadManager {
   Promise<void> enable();
   Promise<void> disable();
   Promise<void> setHeaderValue(ByteString value);
diff --git a/cobalt/worker/service_worker.cc b/cobalt/worker/service_worker.cc
index 4d6ac58..31480b2 100644
--- a/cobalt/worker/service_worker.cc
+++ b/cobalt/worker/service_worker.cc
@@ -17,9 +17,14 @@
 #include <memory>
 #include <utility>
 
+#include "base/bind.h"
+#include "cobalt/base/tokens.h"
 #include "cobalt/script/environment_settings.h"
+#include "cobalt/script/value_handle.h"
+#include "cobalt/web/event_target.h"
 #include "cobalt/web/message_port.h"
 #include "cobalt/worker/service_worker_global_scope.h"
+#include "cobalt/worker/service_worker_jobs.h"
 #include "cobalt/worker/service_worker_object.h"
 #include "cobalt/worker/service_worker_state.h"
 
@@ -30,8 +35,40 @@
                              worker::ServiceWorkerObject* worker)
     : web::EventTarget(settings),
       worker_(worker),
-      message_port_(new web::MessagePort(worker->worker_global_scope())),
       state_(kServiceWorkerStateParsed) {}
 
+void ServiceWorker::PostMessage(const script::ValueHandleHolder& message) {
+  // Algorithm for ServiceWorker.postMessage():
+  //   https://w3c.github.io/ServiceWorker/#service-worker-postmessage
+
+  // 1. Let serviceWorker be the service worker represented by this.
+  ServiceWorkerObject* service_worker = service_worker_object();
+  // 2. Let incumbentSettings be the incumbent settings object.
+  web::EnvironmentSettings* incumbent_settings = environment_settings();
+  // 3. Let incumbentGlobal be incumbentSettings’s global object.
+  // 4. Let serializeWithTransferResult be
+  //    StructuredSerializeWithTransfer(message, options["transfer"]).
+  //    Rethrow any exceptions.
+  std::unique_ptr<script::DataBuffer> serialize_result(
+      script::SerializeScriptValue(message));
+  if (!serialize_result) {
+    return;
+  }
+  // 5. If the result of running the Should Skip Event algorithm with
+  // "message"
+  //    and serviceWorker is true, then return.
+  if (service_worker->ShouldSkipEvent(base::Tokens::message())) return;
+  // 6. Run these substeps in parallel:
+  ServiceWorkerJobs* jobs =
+      incumbent_settings->context()->service_worker_jobs();
+  DCHECK(jobs);
+  jobs->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(&ServiceWorkerJobs::ServiceWorkerPostMessageSubSteps,
+                     base::Unretained(jobs), base::Unretained(service_worker),
+                     base::Unretained(incumbent_settings),
+                     std::move(serialize_result)));
+}
+
 }  // namespace worker
 }  // namespace cobalt
diff --git a/cobalt/worker/service_worker.h b/cobalt/worker/service_worker.h
index a281746..e00424f 100644
--- a/cobalt/worker/service_worker.h
+++ b/cobalt/worker/service_worker.h
@@ -20,6 +20,7 @@
 #include <utility>
 
 #include "cobalt/script/environment_settings.h"
+#include "cobalt/script/value_handle.h"
 #include "cobalt/script/wrappable.h"
 #include "cobalt/web/event_target.h"
 #include "cobalt/web/event_target_listener_info.h"
@@ -43,10 +44,7 @@
 
   // Web API: ServiceWorker
   //
-  void PostMessage(const script::ValueHandleHolder& message) {
-    DCHECK(message_port_);
-    if (worker_->worker_global_scope()) message_port_->PostMessage(message);
-  }
+  void PostMessage(const script::ValueHandleHolder& message);
 
   // The scriptURL getter steps are to return the
   // service worker's serialized script url.
@@ -72,18 +70,16 @@
     SetAttributeEventListener(base::Tokens::error(), event_listener);
   }
 
-  ServiceWorkerObject* service_worker_object() { return worker_; }
+  const scoped_refptr<ServiceWorkerObject>& service_worker_object() {
+    return worker_;
+  }
 
   DEFINE_WRAPPABLE_TYPE(ServiceWorker);
 
  private:
-  ~ServiceWorker() override {
-    message_port_.reset();
-    worker_ = nullptr;
-  }
+  ~ServiceWorker() override { worker_ = nullptr; }
 
   scoped_refptr<ServiceWorkerObject> worker_;
-  scoped_refptr<web::MessagePort> message_port_;
   ServiceWorkerState state_;
 };
 
diff --git a/cobalt/worker/service_worker.idl b/cobalt/worker/service_worker.idl
index b1b9602..f322feb 100644
--- a/cobalt/worker/service_worker.idl
+++ b/cobalt/worker/service_worker.idl
@@ -14,7 +14,7 @@
 
 // https://w3c.github.io/ServiceWorker/#serviceworker-interface
 
-[Exposed = (Window, ServiceWorker)] interface ServiceWorker : EventTarget {
+[Exposed = (Window,ServiceWorker)] interface ServiceWorker : EventTarget {
 
   // Todo: Implement postMessage b/219986442
   void postMessage(any message);
diff --git a/cobalt/worker/service_worker_global_scope_test.cc b/cobalt/worker/service_worker_global_scope_test.cc
index a2a1c0c..0c56208 100644
--- a/cobalt/worker/service_worker_global_scope_test.cc
+++ b/cobalt/worker/service_worker_global_scope_test.cc
@@ -29,6 +29,8 @@
 using script::testing::FakeScriptValue;
 using web::testing::MockEventListener;
 
+namespace {
+
 class ServiceWorkerGlobalScopeTest
     : public testing::TestServiceWorkerWithJavaScript {
  protected:
@@ -41,6 +43,8 @@
   std::unique_ptr<MockEventListener> fake_event_listener_;
 };
 
+}  // namespace
+
 TEST_F(ServiceWorkerGlobalScopeTest, RegistrationIsObject) {
   std::string result;
   EXPECT_TRUE(EvaluateScript("typeof self.registration", &result));
diff --git a/cobalt/worker/service_worker_jobs.cc b/cobalt/worker/service_worker_jobs.cc
index e30f364..41181a2 100644
--- a/cobalt/worker/service_worker_jobs.cc
+++ b/cobalt/worker/service_worker_jobs.cc
@@ -27,12 +27,13 @@
 #include "base/message_loop/message_loop.h"
 #include "base/message_loop/message_loop_current.h"
 #include "base/single_thread_task_runner.h"
+#include "base/strings/string_util.h"
 #include "base/synchronization/lock.h"
 #include "base/task_runner.h"
 #include "base/time/time.h"
 #include "base/trace_event/trace_event.h"
 #include "cobalt/base/tokens.h"
-#include "cobalt/dom/visibility_state.h"
+#include "cobalt/base/type_id.h"
 #include "cobalt/loader/script_loader_factory.h"
 #include "cobalt/network/network_module.h"
 #include "cobalt/script/promise.h"
@@ -47,6 +48,7 @@
 #include "cobalt/worker/client_query_options.h"
 #include "cobalt/worker/client_type.h"
 #include "cobalt/worker/extendable_event.h"
+#include "cobalt/worker/extendable_message_event.h"
 #include "cobalt/worker/frame_type.h"
 #include "cobalt/worker/service_worker.h"
 #include "cobalt/worker/service_worker_container.h"
@@ -56,6 +58,7 @@
 #include "cobalt/worker/service_worker_update_via_cache.h"
 #include "cobalt/worker/window_client.h"
 #include "cobalt/worker/worker_type.h"
+#include "net/base/mime_util.h"
 #include "net/base/url_util.h"
 #include "starboard/atomic.h"
 #include "url/gurl.h"
@@ -240,6 +243,7 @@
 
 void ServiceWorkerJobs::PromiseErrorData::Reject(
     std::unique_ptr<JobPromiseType> promise) const {
+  DCHECK(promise);
   if (message_type_ != script::kNoError) {
     promise->Reject(GetSimpleExceptionType(message_type_));
   } else {
@@ -316,7 +320,7 @@
       DCHECK(last_job);
       base::AutoLock lock(last_job->equivalent_jobs_promise_mutex);
       if (EquivalentJobs(job.get(), last_job) && last_job->promise &&
-          last_job->promise->State() == script::PromiseState::kPending) {
+          last_job->promise->is_pending()) {
         last_job->equivalent_jobs.push_back(std::move(job));
         return;
       }
@@ -558,6 +562,8 @@
   //        environment settings object for this service worker.
   // To perform the fetch given request, run the following steps:
   //   8.1.  Append `Service-Worker`/`script` to request’s header list.
+  net::HttpRequestHeaders headers;
+  headers.SetHeader("Service-Worker", "script");
   //   8.2.  Set request’s cache mode to "no-cache" if any of the following are
   //         true:
   //          - registration’s update via cache mode is not "all".
@@ -569,41 +575,6 @@
   //   8.5.  Set request’s redirect mode to "error".
   //   8.6.  Fetch request, and asynchronously wait to run the remaining steps
   //         as part of fetch’s process response for the response response.
-  //   8.7.  Extract a MIME type from the response’s header list. If this MIME
-  //         type (ignoring parameters) is not a JavaScript MIME type, then:
-  //   8.7.1. Invoke Reject Job Promise with job and "SecurityError"
-  //          DOMException.
-  //   8.7.2. Asynchronously complete these steps with a network error.
-  // TODO(b/235393876): Implement Service-Worker-Allowed.
-  //   8.8.  Let serviceWorkerAllowed be the result of extracting header list
-  //         values given `Service-Worker-Allowed` and response’s header list.
-  //   8.9.  Set policyContainer to the result of creating a policy container
-  //         from a fetch response given response.
-  //   8.10. If serviceWorkerAllowed is failure, then:
-  //   8.10.1  Asynchronously complete these steps with a network error.
-  //   8.11. Let scopeURL be registration’s scope url.
-  //   8.12. Let maxScopeString be null.
-  //   8.13. If serviceWorkerAllowed is null, then:
-  //   8.13.1. Let resolvedScope be the result of parsing "./" using job’s
-  //           script url as the base URL.
-  //   8.13.2. Set maxScopeString to "/", followed by the strings in
-  //           resolvedScope’s path (including empty strings), separated from
-  //           each other by "/".
-  //   8.14. Else:
-  //   8.14.1. Let maxScope be the result of parsing serviceWorkerAllowed using
-  //           job’s script url as the base URL.
-  //   8.14.2. If maxScope’s origin is job’s script url's origin, then:
-  //   8.14.2.1. Set maxScopeString to "/", followed by the strings in
-  //             maxScope’s path (including empty strings), separated from each
-  //             other by "/".
-  //   8.15. Let scopeString be "/", followed by the strings in scopeURL’s path
-  //         (including empty strings), separated from each other by "/".
-  //   8.16. If maxScopeString is null or scopeString does not start with
-  //         maxScopeString, then:
-  //   8.16.1. Invoke Reject Job Promise with job and "SecurityError"
-  //           DOMException.
-  //   8.16.2. Asynchronously complete these steps with a network error.
-
   // TODO(b/225037465): Implement CSP check.
   csp::SecurityCallback csp_callback = base::Bind(&PermitAnyURL);
   loader::Origin origin = loader::Origin(job->script_url.GetOrigin());
@@ -611,8 +582,111 @@
       job->script_url, origin, csp_callback,
       base::Bind(&ServiceWorkerJobs::UpdateOnContentProduced,
                  base::Unretained(this), state),
+      base::Bind(&ServiceWorkerJobs::UpdateOnResponseStarted,
+                 base::Unretained(this), state),
       base::Bind(&ServiceWorkerJobs::UpdateOnLoadingComplete,
-                 base::Unretained(this), state));
+                 base::Unretained(this), state),
+      std::move(headers));
+}
+
+namespace {
+// Array of JavaScript mime types, according to the MIME Sniffinc spec:
+//   https://mimesniff.spec.whatwg.org/#javascript-mime-type
+static const char* const kJavaScriptMimeTypes[] = {"application/ecmascript",
+                                                   "application/javascript",
+                                                   "application/x-ecmascript",
+                                                   "application/x-javascript",
+                                                   "text/ecmascript",
+                                                   "text/javascript",
+                                                   "text/javascript1.0",
+                                                   "text/javascript1.1",
+                                                   "text/javascript1.2",
+                                                   "text/javascript1.3",
+                                                   "text/javascript1.4",
+                                                   "text/javascript1.5",
+                                                   "text/jscript",
+                                                   "text/livescript",
+                                                   "text/x-ecmascript",
+                                                   "text/x-javascript"};
+
+}  // namespace
+
+bool ServiceWorkerJobs::UpdateOnResponseStarted(
+    scoped_refptr<UpdateJobState> state, loader::Fetcher* fetcher,
+    const scoped_refptr<net::HttpResponseHeaders>& headers) {
+  std::string content_type;
+  bool mime_type_is_javascript = false;
+  if (headers->GetNormalizedHeader("Content-type", &content_type)) {
+    //   8.7.  Extract a MIME type from the response’s header list. If this MIME
+    //         type (ignoring parameters) is not a JavaScript MIME type, then:
+    for (auto mime_type : kJavaScriptMimeTypes) {
+      if (net::MatchesMimeType(mime_type, content_type)) {
+        mime_type_is_javascript = true;
+        break;
+      }
+    }
+  }
+  if (!mime_type_is_javascript) {
+    //   8.7.1. Invoke Reject Job Promise with job and "SecurityError"
+    //          DOMException.
+    //   8.7.2. Asynchronously complete these steps with a network error.
+    RejectJobPromise(state->job,
+                     PromiseErrorData(web::DOMException::kSecurityErr,
+                                      "Service Worker Script is not "
+                                      "JavaScript MIME type."));
+    return true;
+  }
+  //   8.8.  Let serviceWorkerAllowed be the resulf extracting header list
+  //         values given `Service-Worker-Allowed` and response’s header list.
+  std::string service_worker_allowed;
+  bool service_worker_allowed_exists = headers->GetNormalizedHeader(
+      "Service-Worker-Allowed", &service_worker_allowed);
+  //   8.9.  Set policyContainer to the result of creating a policy container
+  //         from a fetch response given response.
+  //   8.10. If serviceWorkerAllowed is failure, then:
+  //   8.10.1  Asynchronously complete these steps with a network error.
+  //   8.11. Let scopeURL be registration’s scope url.
+  GURL scope_url = state->registration->scope_url();
+  //   8.12. Let maxScopeString be null.
+  base::Optional<std::string> max_scope_string;
+  //   8.13. If serviceWorkerAllowed is null, then:
+  if (!service_worker_allowed_exists || service_worker_allowed.empty()) {
+    //   8.13.1. Let resolvedScope be the result of parsing "./" using job’s
+    //           script url as the base URL.
+    //   8.13.2. Set maxScopeString to "/", followed by the strings in
+    //           resolvedScope’s path (including empty strings), separated
+    //           from each other by "/".
+    max_scope_string = state->job->script_url.GetWithoutFilename().path();
+  } else {
+    //   8.14. Else:
+    //   8.14.1. Let maxScope be the result of parsing serviceWorkerAllowed
+    //           using job’s script url as the base URL.
+    GURL max_scope = state->job->script_url.Resolve(service_worker_allowed);
+    //   8.14.2. If maxScope’s origin is job’s script url's origin, then:
+    if (loader::Origin(state->job->script_url) == loader::Origin(max_scope)) {
+      //   8.14.2.1. Set maxScopeString to "/", followed by the strings in
+      //             maxScope’s path (including empty strings), separated from
+      //             each other by "/".
+      max_scope_string = max_scope.path();
+    }
+  }
+  //   8.15. Let scopeString be "/", followed by the strings in scopeURL’s
+  //         path (including empty strings), separated from each other by "/".
+  std::string scope_string = scope_url.path();
+  //   8.16. If maxScopeString is null or scopeString does not start with
+  //         maxScopeString, then:
+  if (!max_scope_string.has_value() ||
+      !base::StartsWith(scope_string, max_scope_string.value(),
+                        base::CompareCase::SENSITIVE)) {
+    //   8.16.1. Invoke Reject Job Promise with job and "SecurityError"
+    //           DOMException.
+    //   8.16.2. Asynchronously complete these steps with a network error.
+    RejectJobPromise(state->job,
+                     PromiseErrorData(web::DOMException::kSecurityErr,
+                                      "Scope not allowed."));
+    return true;
+  }
+  return true;
 }
 
 void ServiceWorkerJobs::UpdateOnContentProduced(
@@ -658,9 +732,28 @@
   TRACE_EVENT0("cobalt::worker",
                "ServiceWorkerJobs::UpdateOnLoadingComplete()");
   DCHECK_EQ(message_loop(), base::MessageLoop::current());
+  if (state->job->promise.get() == nullptr) {
+    // The job is already rejected, which means there was an error, so finish
+    // the job and skip the remaining steps.
+    FinishJob(state->job);
+    return;
+  }
+
+  if (error) {
+    RejectJobPromise(
+        state->job,
+        PromiseErrorData(web::DOMException::kNetworkErr, error.value()));
+    if (state->newest_worker == nullptr) {
+      scope_to_registration_map_.RemoveRegistration(state->job->storage_key,
+                                                    state->job->scope_url);
+    }
+    FinishJob(state->job);
+    return;
+  }
+
   //   8.21. If hasUpdatedResources is false and newestWorker’s classic
   //         scripts imported flag is set, then:
-  if (!state->has_updated_resources &&
+  if (!state->has_updated_resources && state->newest_worker &&
       state->newest_worker->classic_scripts_imported()) {
     // This checks if there are any updates to already stored importScripts
     // resources.
@@ -883,7 +976,7 @@
   ServiceWorkerObject* installing_worker = registration->installing_worker();
   // 11. If the result of running the Should Skip Event algorithm with
   //     installingWorker and "install" is false, then:
-  if (!ShouldSkipEvent(base::Tokens::install(), installing_worker)) {
+  if (!installing_worker->ShouldSkipEvent(base::Tokens::install())) {
     // 11.1. Let forceBypassCache be true if job’s force bypass cache flag is
     //       set, and false otherwise.
     bool force_bypass_cache = job->force_bypass_cache_flag;
@@ -1162,7 +1255,7 @@
   ServiceWorkerObject* active_worker = registration->active_worker();
   // 11. If the result of running the Should Skip Event algorithm with
   //     activeWorker and "activate" is false, then:
-  if (!ShouldSkipEvent(base::Tokens::activate(), active_worker)) {
+  if (!active_worker->ShouldSkipEvent(base::Tokens::activate())) {
     // 11.1. If the result of running the Run Service Worker algorithm with
     //       activeWorker is not failure, then:
     auto* run_result = RunServiceWorker(active_worker);
@@ -1510,15 +1603,6 @@
   }
 }
 
-bool ServiceWorkerJobs::ShouldSkipEvent(base::Token event_name,
-                                        ServiceWorkerObject* worker) {
-  // Algorithm for Should Skip Event:
-  //   https://w3c.github.io/ServiceWorker/#should-skip-event-algorithm
-  // TODO(b/229622132): Implementing this algorithm will improve performance.
-  NOTIMPLEMENTED();
-  return false;
-}
-
 void ServiceWorkerJobs::HandleServiceWorkerClientUnload(
     web::EnvironmentSettings* client) {
   TRACE_EVENT0("cobalt::worker",
@@ -2050,22 +2134,16 @@
           [](web::EnvironmentSettings* client, web::Context* promise_context,
              std::unique_ptr<script::ValuePromiseWrappable::Reference>
                  promise_reference) {
-            std::unique_ptr<WindowData> window_data(new WindowData);
-            window_data->client = client;
             // 4.4.1. Let frameType be the result of running Get Frame Type with
             //        browsingContext.
-            // Cobalt does not support nested or auxiliary
-            // browsing contexts.
-            window_data->frame_type = kFrameTypeTopLevel;
-
+            // Cobalt does not support nested or auxiliary browsing contexts.
             // 4.4.2. Let visibilityState be browsingContext’s active document's
             //        visibilityState attribute value.
-            // TODO(b/235838698): Implement WindowClient.visibilityState.
-
             // 4.4.3. Let focusState be the result of running the has focus
             //        steps with browsingContext’s active document as the
             //        argument.
-            // TODO(b/235838698): Implement WindowClient.focused.
+            // Handled in the WindowData constructor.
+            std::unique_ptr<WindowData> window_data(new WindowData(client));
 
             // 4.4.4. Let ancestorOriginsList be the empty list.
             // 4.4.5. If client is a window client, set ancestorOriginsList to
@@ -2090,7 +2168,7 @@
                       //          Create Window Client with client,
                       //          frameType, visibilityState, focusState,
                       //          and ancestorOriginsList.
-                      scoped_refptr<WindowClient> window_client =
+                      scoped_refptr<Client> window_client =
                           WindowClient::Create(*window_data);
                       // 4.4.6.3. Resolve promise with windowClient.
                       promise_reference->value().Resolve(window_client);
@@ -2174,8 +2252,7 @@
 
       // 2.5.1.1. Let windowData be [ "client" -> client, "ancestorOriginsList"
       //          -> a new list ].
-      WindowData window_data;
-      window_data.client = client;
+      WindowData window_data(client);
 
       // 2.5.1.2. Let browsingContext be null.
 
@@ -2217,19 +2294,13 @@
                            //            browsingContext.
                            // Cobalt does not support nested or auxiliary
                            // browsing contexts.
-                           window_data->frame_type = kFrameTypeTopLevel;
-
                            // 2.5.1.6.4. Set windowData["visibilityState"] to
                            //            browsingContext’s active document's
                            //            visibilityState attribute value.
-                           // TODO(b/235838698): Implement
-                           // WindowClient.visibilityState.
-
                            // 2.5.1.6.5. Set windowData["focusState"] to the
                            //            result of running the has focus steps
                            //            with browsingContext’s active document
                            //            as the argument.
-                           // TODO(b/235838698): Implement WindowClient.focused.
 
                            // 2.5.1.6.6. If client is a window client, then set
                            //            windowData["ancestorOriginsList"] to
@@ -2288,9 +2359,8 @@
               //          windowData["focusState"], and
               //          windowData["ancestorOriginsList"] as the
               //          arguments.
-              // TODO(b/235838698): Implement WindowCLient properties
-              // and methods.
-              scoped_refptr<WindowClient> window_client =
+              // TODO(b/235838698): Implement WindowClient methods.
+              scoped_refptr<Client> window_client =
                   WindowClient::Create(window_data);
 
               // 2.6.2.2. Append WindowClient to clientObjects.
@@ -2412,6 +2482,90 @@
           std::move(promise_reference)));
 }
 
+void ServiceWorkerJobs::ServiceWorkerPostMessageSubSteps(
+    ServiceWorkerObject* service_worker,
+    web::EnvironmentSettings* incumbent_settings,
+    std::unique_ptr<script::DataBuffer> serialize_result) {
+  // Parallel sub steps (6) for algorithm for ServiceWorker.postMessage():
+  //   https://w3c.github.io/ServiceWorker/#service-worker-postmessage-options
+  // 3. Let incumbentGlobal be incumbentSettings’s global object.
+  // 6.1 If the result of running the Run Service Worker algorithm with
+  //     serviceWorker is failure, then return.
+  auto* run_result = RunServiceWorker(service_worker);
+  if (!run_result) return;
+
+  // 6.2 Queue a task on the DOM manipulation task source to run the following
+  //     steps:
+  incumbent_settings->context()->message_loop()->task_runner()->PostTask(
+      FROM_HERE,
+      base::BindOnce(
+          [](ServiceWorkerObject* service_worker,
+             web::EnvironmentSettings* incumbent_settings,
+             std::unique_ptr<script::DataBuffer> serialize_result) {
+            web::WindowOrWorkerGlobalScope* incumbent_global =
+                incumbent_settings->context()->GetWindowOrWorkerGlobalScope();
+
+            web::EventTarget* event_target =
+                service_worker->worker_global_scope();
+            if (!event_target) return;
+
+            ExtendableMessageEventInit init_dict;
+            if (incumbent_global->GetWrappableType() ==
+                base::GetTypeId<ServiceWorkerGlobalScope>()) {
+              // 6.2.1. Let source be determined by switching on the
+              //        type of incumbentGlobal:
+              //        . ServiceWorkerGlobalScope
+              //          The result of getting the service worker
+              //          object that represents incumbentGlobal’s
+              //          service worker in the relevant settings
+              //          object of serviceWorker’s global object.
+              init_dict.set_source(ExtendableMessageEvent::SourceType(
+                  event_target->environment_settings()
+                      ->context()
+                      ->GetServiceWorker(incumbent_global->AsServiceWorker()
+                                             ->service_worker_object())));
+            } else if (incumbent_global->GetWrappableType() ==
+                       base::GetTypeId<dom::Window>()) {
+              //        . Window
+              //          a new WindowClient object that represents
+              //          incumbentGlobal’s relevant settings object.
+              init_dict.set_source(
+                  ExtendableMessageEvent::SourceType(WindowClient::Create(
+                      WindowData(incumbent_global->environment_settings()))));
+            } else {
+              //        . Otherwise
+              //          a new Client object that represents
+              //          incumbentGlobal’s associated worker
+              init_dict.set_source(ExtendableMessageEvent::SourceType(
+                  Client::Create(incumbent_global->environment_settings())));
+            }
+
+            base::MessageLoop* message_loop =
+                event_target->environment_settings()->context()->message_loop();
+            if (!message_loop) {
+              return;
+            }
+            if (!serialize_result) {
+              return;
+            }
+            message_loop->task_runner()->PostTask(
+                FROM_HERE,
+                base::BindOnce(
+                    [](const ExtendableMessageEventInit& init_dict,
+                       web::EventTarget* event_target,
+                       std::unique_ptr<script::DataBuffer> serialize_result) {
+                      event_target->DispatchEvent(
+                          new worker::ExtendableMessageEvent(
+                              base::Tokens::message(), init_dict,
+                              std::move(serialize_result)));
+                    },
+                    init_dict, base::Unretained(event_target),
+                    std::move(serialize_result)));
+          },
+          base::Unretained(service_worker),
+          base::Unretained(incumbent_settings), std::move(serialize_result)));
+}
+
 void ServiceWorkerJobs::RegisterWebContext(web::Context* context) {
   DCHECK_NE(nullptr, context);
   web_context_registrations_cleared_.Reset();
@@ -2455,22 +2609,26 @@
 
 void ServiceWorkerJobs::JobPromiseType::Resolve(const bool result) {
   DCHECK(promise_bool_reference_);
+  is_pending_.store(false);
   promise_bool_reference_->value().Resolve(result);
 }
 
 void ServiceWorkerJobs::JobPromiseType::Resolve(
     const scoped_refptr<cobalt::script::Wrappable>& result) {
   DCHECK(promise_wrappable_reference_);
+  is_pending_.store(false);
   promise_wrappable_reference_->value().Resolve(result);
 }
 
 void ServiceWorkerJobs::JobPromiseType::Reject(
     script::SimpleExceptionType exception) {
   if (promise_bool_reference_) {
+    is_pending_.store(false);
     promise_bool_reference_->value().Reject(exception);
     return;
   }
   if (promise_wrappable_reference_) {
+    is_pending_.store(false);
     promise_wrappable_reference_->value().Reject(exception);
     return;
   }
@@ -2480,26 +2638,17 @@
 void ServiceWorkerJobs::JobPromiseType::Reject(
     const scoped_refptr<script::ScriptException>& result) {
   if (promise_bool_reference_) {
+    is_pending_.store(false);
     promise_bool_reference_->value().Reject(result);
     return;
   }
   if (promise_wrappable_reference_) {
+    is_pending_.store(false);
     promise_wrappable_reference_->value().Reject(result);
     return;
   }
   NOTREACHED();
 }
 
-script::PromiseState ServiceWorkerJobs::JobPromiseType::State() {
-  if (promise_bool_reference_) {
-    return promise_bool_reference_->value().State();
-  }
-  if (promise_wrappable_reference_) {
-    return promise_wrappable_reference_->value().State();
-  }
-  NOTREACHED();
-  return script::PromiseState::kPending;
-}
-
 }  // namespace worker
 }  // namespace cobalt
diff --git a/cobalt/worker/service_worker_jobs.h b/cobalt/worker/service_worker_jobs.h
index 11b037b..c4fa601 100644
--- a/cobalt/worker/service_worker_jobs.h
+++ b/cobalt/worker/service_worker_jobs.h
@@ -48,6 +48,7 @@
 #include "cobalt/worker/service_worker_registration_object.h"
 #include "cobalt/worker/service_worker_update_via_cache.h"
 #include "cobalt/worker/worker_type.h"
+#include "starboard/atomic.h"
 #include "url/gurl.h"
 #include "url/origin.h"
 
@@ -83,11 +84,14 @@
     void Resolve(const bool result);
     void Resolve(const scoped_refptr<cobalt::script::Wrappable>& result);
     void Reject(script::SimpleExceptionType exception);
+    void Reject(web::DOMException::ExceptionCode code,
+                const std::string& message);
     void Reject(const scoped_refptr<script::ScriptException>& result);
 
-    script::PromiseState State();
+    bool is_pending() const { return is_pending_.load(); }
 
    private:
+    starboard::atomic_bool is_pending_{true};
     std::unique_ptr<script::ValuePromiseBool::Reference>
         promise_bool_reference_;
     std::unique_ptr<script::ValuePromiseWrappable::Reference>
@@ -204,7 +208,7 @@
       const base::WeakPtr<ServiceWorkerObject>& service_worker,
       std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
 
-  // Sub steps for WaitUntil.
+  // Sub steps for ExtendableEvent.WaitUntil().
   //   https://w3c.github.io/ServiceWorker/#dom-extendableevent-waituntil
   void WaitUntilSubSteps(ServiceWorkerRegistrationObject* registration);
 
@@ -240,6 +244,13 @@
       ServiceWorkerObject* associated_service_worker,
       std::unique_ptr<script::ValuePromiseVoid::Reference> promise_reference);
 
+  // Parallel sub steps (6) for algorithm for ServiceWorker.postMessage():
+  //   https://w3c.github.io/ServiceWorker/#service-worker-postmessage-options
+  void ServiceWorkerPostMessageSubSteps(
+      ServiceWorkerObject* service_worker,
+      web::EnvironmentSettings* incumbent_settings,
+      std::unique_ptr<script::DataBuffer> serialize_result);
+
   // Registration of web contexts that may have service workers.
   void RegisterWebContext(web::Context* context);
   void UnregisterWebContext(web::Context* context);
@@ -344,6 +355,9 @@
   void UpdateOnContentProduced(scoped_refptr<UpdateJobState> state,
                                const loader::Origin& last_url_origin,
                                std::unique_ptr<std::string> content);
+  bool UpdateOnResponseStarted(
+      scoped_refptr<UpdateJobState> state, loader::Fetcher* fetcher,
+      const scoped_refptr<net::HttpResponseHeaders>& headers);
   void UpdateOnLoadingComplete(scoped_refptr<UpdateJobState> state,
                                const base::Optional<std::string>& error);
 
@@ -400,9 +414,6 @@
   // https://w3c.github.io/ServiceWorker/#update-state-algorithm
   void UpdateWorkerState(ServiceWorkerObject* worker, ServiceWorkerState state);
 
-  // https://w3c.github.io/ServiceWorker/#should-skip-event-algorithm
-  bool ShouldSkipEvent(base::Token event_name, ServiceWorkerObject* worker);
-
   // https://w3c.github.io/ServiceWorker/#on-client-unload-algorithm
   void HandleServiceWorkerClientUnload(web::EnvironmentSettings* client);
 
diff --git a/cobalt/worker/service_worker_object.cc b/cobalt/worker/service_worker_object.cc
index e63c421..34c2005 100644
--- a/cobalt/worker/service_worker_object.cc
+++ b/cobalt/worker/service_worker_object.cc
@@ -132,6 +132,14 @@
   web_agent_->WaitUntilDone();
 }
 
+bool ServiceWorkerObject::ShouldSkipEvent(base::Token event_name) {
+  // Algorithm for Should Skip Event:
+  //   https://w3c.github.io/ServiceWorker/#should-skip-event-algorithm
+  // TODO(b/229622132): Implementing this algorithm will improve performance.
+  NOTIMPLEMENTED();
+  return false;
+}
+
 void ServiceWorkerObject::Initialize(web::Context* context) {
   TRACE_EVENT0("cobalt::worker", "ServiceWorkerObject::Initialize()");
   // Algorithm for "Run Service Worker"
diff --git a/cobalt/worker/service_worker_object.h b/cobalt/worker/service_worker_object.h
index a3cf5cb..c9b0c1a 100644
--- a/cobalt/worker/service_worker_object.h
+++ b/cobalt/worker/service_worker_object.h
@@ -136,6 +136,10 @@
 
   void ObtainWebAgentAndWaitUntilDone();
 
+  // Algorithm for Should Skip Event:
+  //   https://w3c.github.io/ServiceWorker/#should-skip-event-algorithm
+  bool ShouldSkipEvent(base::Token event_name);
+
  private:
   // Called by ObtainWebAgentAndWaitUntilDone to perform initialization required
   // on the dedicated thread.
diff --git a/cobalt/worker/testing/test_with_javascript.h b/cobalt/worker/testing/test_with_javascript.h
index f6499d5..72dda4c 100644
--- a/cobalt/worker/testing/test_with_javascript.h
+++ b/cobalt/worker/testing/test_with_javascript.h
@@ -83,23 +83,34 @@
     web_context_.reset();
   }
 
-  WorkerGlobalScope* worker_global_scope() { return worker_global_scope_; }
-
-  virtual scoped_refptr<script::GlobalEnvironment> global_environment() {
-    return web_context_->global_environment();
+  WorkerGlobalScope* worker_global_scope() const {
+    return worker_global_scope_;
   }
 
-  bool EvaluateScript(const std::string& js_code, std::string* result) {
-    DCHECK(this->global_environment());
-    scoped_refptr<script::SourceCode> source_code =
-        script::SourceCode::CreateSourceCode(
-            js_code, base::SourceLocation(__FILE__, __LINE__, 1));
+  virtual web::Context* web_context() const {
+    DCHECK(web_context_);
+    return web_context_.get();
+  }
 
-    this->global_environment()->EnableEval();
-    this->global_environment()->SetReportEvalCallback(base::Closure());
-    bool succeeded =
-        this->global_environment()->EvaluateScript(source_code, result);
-    return succeeded;
+  scoped_refptr<script::GlobalEnvironment> global_environment() const {
+    DCHECK(this->web_context());
+    return this->web_context()->global_environment();
+  }
+
+  bool EvaluateScript(const std::string& js_code,
+                      std::string* result = nullptr) {
+    DCHECK(global_environment());
+    return global_environment()->EvaluateScript(
+        CreateSourceCodeAndPrepareEval(js_code), result);
+  }
+
+  bool EvaluateScript(
+      const std::string& js_code,
+      base::Optional<script::ValueHandleHolder::Reference>* result) {
+    DCHECK(global_environment());
+    return global_environment()->EvaluateScript(
+        CreateSourceCodeAndPrepareEval(js_code),
+        this->web_context()->GetWindowOrWorkerGlobalScope(), result);
   }
 
   ::testing::StrictMock<script::testing::MockExceptionState>*
@@ -108,6 +119,15 @@
   }
 
  private:
+  scoped_refptr<script::SourceCode> CreateSourceCodeAndPrepareEval(
+      const std::string& js_code) {
+    DCHECK(global_environment());
+    global_environment()->EnableEval();
+    global_environment()->SetReportEvalCallback(base::Closure());
+    return script::SourceCode::CreateSourceCode(
+        js_code, base::SourceLocation(__FILE__, __LINE__, 1));
+  }
+
   std::unique_ptr<web::testing::StubWebContext> web_context_;
   WorkerGlobalScope* worker_global_scope_ = nullptr;
   scoped_refptr<DedicatedWorkerGlobalScope> dedicated_worker_global_scope_;
diff --git a/cobalt/worker/window_client.cc b/cobalt/worker/window_client.cc
index 194ef1d..a0dc031 100644
--- a/cobalt/worker/window_client.cc
+++ b/cobalt/worker/window_client.cc
@@ -14,6 +14,12 @@
 
 #include "cobalt/worker/window_client.h"
 
+#include "cobalt/dom/document.h"
+#include "cobalt/dom/visibility_state.h"
+#include "cobalt/dom/window.h"
+#include "cobalt/web/environment_settings.h"
+#include "cobalt/web/window_or_worker_global_scope.h"
+
 namespace cobalt {
 namespace worker {
 
@@ -31,7 +37,7 @@
 
   // 4. Set windowClient’s visibility state to visibilityState.
   // 5. Set windowClient’s focus state to focusState.
-  // TODO(b/235838698): Implement WindowCLient properties and methods.
+  // Determined in the property getter.
 
   // 6. Set windowClient’s ancestor origins array to a frozen array created from
   //    ancestorOriginsList.
@@ -40,5 +46,81 @@
   // 7. Return windowClient.
 }
 
+dom::VisibilityState WindowClient::visibility_state() {
+  DCHECK(event_target());
+  DCHECK(event_target()->environment_settings());
+  web::Context* context = event_target()->environment_settings()->context();
+  DCHECK(context);
+  base::MessageLoop* message_loop = context->message_loop();
+  if (!message_loop) {
+    return dom::kVisibilityStateHidden;
+  }
+
+  dom::VisibilityState visibility_state = dom::kVisibilityStateHidden;
+  if (message_loop->task_runner()->BelongsToCurrentThread()) {
+    DCHECK(context->GetWindowOrWorkerGlobalScope()->IsWindow());
+    DCHECK(context->GetWindowOrWorkerGlobalScope()->AsWindow()->document());
+    visibility_state = context->GetWindowOrWorkerGlobalScope()
+                           ->AsWindow()
+                           ->document()
+                           ->visibility_state();
+  } else {
+    message_loop->task_runner()->PostBlockingTask(
+        FROM_HERE,
+        base::Bind(
+            [](web::Context* context, dom::VisibilityState* visibility_state) {
+              DCHECK(context->GetWindowOrWorkerGlobalScope()->IsWindow());
+              DCHECK(context->GetWindowOrWorkerGlobalScope()
+                         ->AsWindow()
+                         ->document());
+              *visibility_state = context->GetWindowOrWorkerGlobalScope()
+                                      ->AsWindow()
+                                      ->document()
+                                      ->visibility_state();
+            },
+            context, &visibility_state));
+  }
+
+  return visibility_state;
+}
+
+bool WindowClient::focused() {
+  DCHECK(event_target());
+  DCHECK(event_target()->environment_settings());
+  web::Context* context = event_target()->environment_settings()->context();
+  DCHECK(context);
+  base::MessageLoop* message_loop = context->message_loop();
+  if (!message_loop) {
+    return false;
+  }
+
+  bool focused = false;
+  if (message_loop->task_runner()->BelongsToCurrentThread()) {
+    DCHECK(context->GetWindowOrWorkerGlobalScope()->IsWindow());
+    DCHECK(context->GetWindowOrWorkerGlobalScope()->AsWindow()->document());
+    focused = context->GetWindowOrWorkerGlobalScope()
+                  ->AsWindow()
+                  ->document()
+                  ->HasFocus();
+  } else {
+    message_loop->task_runner()->PostBlockingTask(
+        FROM_HERE,
+        base::Bind(
+            [](web::Context* context, bool* focused) {
+              DCHECK(context->GetWindowOrWorkerGlobalScope()->IsWindow());
+              DCHECK(context->GetWindowOrWorkerGlobalScope()
+                         ->AsWindow()
+                         ->document());
+              *focused = context->GetWindowOrWorkerGlobalScope()
+                             ->AsWindow()
+                             ->document()
+                             ->HasFocus();
+            },
+            context, &focused));
+  }
+
+  return focused;
+}
+
 }  // namespace worker
 }  // namespace cobalt
diff --git a/cobalt/worker/window_client.h b/cobalt/worker/window_client.h
index 4fcde90..fe1eacc 100644
--- a/cobalt/worker/window_client.h
+++ b/cobalt/worker/window_client.h
@@ -15,6 +15,7 @@
 #ifndef COBALT_WORKER_WINDOW_CLIENT_H_
 #define COBALT_WORKER_WINDOW_CLIENT_H_
 
+#include "cobalt/dom/visibility_state.h"
 #include "cobalt/web/environment_settings.h"
 #include "cobalt/worker/client.h"
 #include "cobalt/worker/frame_type.h"
@@ -23,6 +24,11 @@
 namespace worker {
 
 struct WindowData {
+ public:
+  WindowData(web::EnvironmentSettings* client = nullptr,
+             FrameType frame_type = kFrameTypeTopLevel)
+      : client(client), frame_type(frame_type) {}
+
   web::EnvironmentSettings* client = nullptr;
   FrameType frame_type = kFrameTypeTopLevel;
 };
@@ -30,15 +36,20 @@
 class WindowClient : public Client {
  public:
   // https://w3c.github.io/ServiceWorker/#create-window-client
-  static WindowClient* Create(const WindowData& window_data) {
+  static scoped_refptr<Client> Create(const WindowData& window_data) {
     return new WindowClient(window_data);
   }
-  // TODO(b/235838698): Implement WindowCLient properties and methods.
+  dom::VisibilityState visibility_state();
+  bool focused();
 
+  // TODO(b/235838698): Implement WindowClient methods.
   DEFINE_WRAPPABLE_TYPE(WindowClient);
 
  private:
   explicit WindowClient(const WindowData& window_data);
+
+  dom::VisibilityState visibility_state_ = dom::kVisibilityStateVisible;
+  bool focused_ = true;
 };
 
 
diff --git a/cobalt/worker/window_client.idl b/cobalt/worker/window_client.idl
index 696b272..3804274 100644
--- a/cobalt/worker/window_client.idl
+++ b/cobalt/worker/window_client.idl
@@ -15,9 +15,9 @@
 // https://w3c.github.io/ServiceWorker/#client-interface
 
 [Exposed = ServiceWorker] interface WindowClient : Client {
-  // TODO(b/235838698): Implement WindowCLient properties and methods.
-  // readonly attribute VisibilityState visibilityState;
-  // readonly attribute boolean focused;
+  readonly attribute VisibilityState visibilityState;
+  readonly attribute boolean focused;
+  // TODO(b/235838698): Implement WindowClient methods.
   // [NewObject] Promise<WindowClient> focus();
   // [NewObject] Promise<WindowClient?> navigate(USVString url);
 };
diff --git a/cobalt/worker/worker.cc b/cobalt/worker/worker.cc
index a716910..870783d 100644
--- a/cobalt/worker/worker.cc
+++ b/cobalt/worker/worker.cc
@@ -176,6 +176,8 @@
   loader_ = web_context_->script_loader_factory()->CreateScriptLoader(
       url, origin, csp_callback,
       base::Bind(&Worker::OnContentProduced, base::Unretained(this)),
+      base::Bind(&WorkerGlobalScope::InitializePolicyContainerCallback,
+                 worker_global_scope_),
       base::Bind(&Worker::OnLoadingComplete, base::Unretained(this)));
 }
 
diff --git a/cobalt/worker/worker_global_scope.cc b/cobalt/worker/worker_global_scope.cc
index 662aaa5..4c68bbe 100644
--- a/cobalt/worker/worker_global_scope.cc
+++ b/cobalt/worker/worker_global_scope.cc
@@ -23,6 +23,7 @@
 #include "base/message_loop/message_loop_current.h"
 #include "base/threading/thread.h"
 #include "base/trace_event/trace_event.h"
+#include "cobalt/loader/net_fetcher.h"
 #include "cobalt/loader/origin.h"
 #include "cobalt/script/environment_settings.h"
 #include "cobalt/web/context.h"
@@ -195,6 +196,53 @@
   set_navigator_base(navigator_);
 }
 
+bool WorkerGlobalScope::InitializePolicyContainerCallback(
+    loader::Fetcher* fetcher,
+    const scoped_refptr<net::HttpResponseHeaders>& headers) {
+  DCHECK(headers);
+  // https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#initialize-worker-policy-container
+  // 1. If workerGlobalScope's url is local but its scheme is not "blob":
+  //   1. Assert: workerGlobalScope's owner set's size is 1.
+  //   2. Set workerGlobalScope's policy container to a clone of
+  //      workerGlobalScope's owner set[0]'s relevant settings object's policy
+  //      container.
+  // 2. Otherwise, set workerGlobalScope's policy container to the result of
+  //    creating a policy container from a fetch response given response and
+  //    environment.
+  // Steps from create a policy container from a fetch response:
+  // https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#creating-a-policy-container-from-a-fetch-response
+  // 1. If response's URL's scheme is "blob", then return a clone of response's
+  //    URL's blob URL entry's environment's policy container.
+  // 2. Let result be a new policy container.
+  // 3. Set result's CSP list to the result of parsing a response's Content
+  //    Security Policies given response.
+  // 4. If environment is non-null, then set result's embedder policy to the
+  //    result of obtaining an embedder policy given response and environment.
+  //    Otherwise, set it to "unsafe-none".
+  // 5. Set result's referrer policy to the result of parsing the
+  //    `Referrer-Policy` header given response. [REFERRERPOLICY]
+  // 6. Return result.
+
+  // Steps 3-6. Since csp_delegate doesn't fully mirror PolicyContainer, we
+  // don't create a new one here and return it. Instead we update the existing
+  // one for this worker and return true for success and false for failure.
+  csp::ResponseHeaders csp_headers(headers);
+  if (csp_delegate()->OnReceiveHeaders(csp_headers)) {
+    return true;
+  }
+  // Only NetFetchers are expected to call this, since only they have the
+  // response headers.
+  loader::NetFetcher* net_fetcher =
+      base::polymorphic_downcast<loader::NetFetcher*>(fetcher);
+  net::URLFetcher* url_fetcher = net_fetcher->url_fetcher();
+  LOG(INFO) << "Failure receiving Content Security Policy headers "
+               "for URL: "
+            << url_fetcher->GetURL() << ".";
+  // Return true regardless of CSP headers being received to continue loading
+  // the response.
+  return true;
+}
+
 void WorkerGlobalScope::ImportScripts(const std::vector<std::string>& urls,
                                       script::ExceptionState* exception_state) {
   ImportScriptsInternal(urls, exception_state, URLLookupCallback(),
diff --git a/cobalt/worker/worker_global_scope.h b/cobalt/worker/worker_global_scope.h
index f4e2291..345220b 100644
--- a/cobalt/worker/worker_global_scope.h
+++ b/cobalt/worker/worker_global_scope.h
@@ -63,6 +63,13 @@
 
   virtual void Initialize() {}
 
+  // https://html.spec.whatwg.org/commit-snapshots/465a6b672c703054de278b0f8133eb3ad33d93f4/#initialize-worker-policy-container
+  // Intended to be called from a loader::Decoder::OnResponseStarted to
+  // initialize CSP headers once they're fetched.
+  bool InitializePolicyContainerCallback(
+      loader::Fetcher* fetcher,
+      const scoped_refptr<net::HttpResponseHeaders>& headers);
+
   // Web API: WorkerGlobalScope
   //
   scoped_refptr<WorkerGlobalScope> self() { return this; }
diff --git a/cobalt/worker/worker_global_scope_test.cc b/cobalt/worker/worker_global_scope_test.cc
index 67ee3ed..216da15 100644
--- a/cobalt/worker/worker_global_scope_test.cc
+++ b/cobalt/worker/worker_global_scope_test.cc
@@ -20,6 +20,7 @@
 #include "cobalt/bindings/testing/utils.h"
 #include "cobalt/script/testing/fake_script_value.h"
 #include "cobalt/web/error_event.h"
+#include "cobalt/web/message_event.h"
 #include "cobalt/web/testing/mock_event_listener.h"
 #include "cobalt/worker/testing/test_with_javascript.h"
 
diff --git a/cobalt/xhr/xml_http_request.cc b/cobalt/xhr/xml_http_request.cc
index d943154..551bd8c8 100644
--- a/cobalt/xhr/xml_http_request.cc
+++ b/cobalt/xhr/xml_http_request.cc
@@ -27,7 +27,6 @@
 #include "cobalt/base/polymorphic_downcast.h"
 #include "cobalt/base/source_location.h"
 #include "cobalt/base/tokens.h"
-#include "cobalt/dom/dom_settings.h"
 #include "cobalt/dom/performance.h"
 #include "cobalt/dom/progress_event.h"
 #include "cobalt/dom/window.h"
@@ -188,19 +187,12 @@
     : XMLHttpRequestEventTarget(settings) {
   // Determine which implementation of XHR to use based on being in a window or
   // worker.
-  if (environment_settings()
-          ->context()
-          ->GetWindowOrWorkerGlobalScope()
-          ->IsWindow()) {
+  web::WindowOrWorkerGlobalScope* window_or_worker_global_scope =
+      environment_settings()->context()->GetWindowOrWorkerGlobalScope();
+  if (window_or_worker_global_scope->IsWindow()) {
     xhr_impl_ = std::make_unique<DOMXMLHttpRequestImpl>(this);
-  } else if (environment_settings()
-                 ->context()
-                 ->GetWindowOrWorkerGlobalScope()
-                 ->IsDedicatedWorker() ||
-             environment_settings()
-                 ->context()
-                 ->GetWindowOrWorkerGlobalScope()
-                 ->IsServiceWorker()) {
+  } else if (window_or_worker_global_scope->IsDedicatedWorker() ||
+             window_or_worker_global_scope->IsServiceWorker()) {
     xhr_impl_ = std::make_unique<XMLHttpRequestImpl>(this);
   }
   xhr::GlobalStats::GetInstance()->Add(this);
@@ -367,7 +359,7 @@
       stop_timeout_(false),
       timeout_ms_(0),
       upload_complete_(false) {
-  DCHECK(settings_);
+  DCHECK(environment_settings());
 }
 
 void XMLHttpRequestImpl::Abort() {
@@ -411,7 +403,7 @@
     return;
   }
 
-  base_url_ = settings_->base_url();
+  base_url_ = environment_settings()->base_url();
 
   if (IsForbiddenMethod(method)) {
     web::DOMException::Raise(web::DOMException::kSecurityErr, exception_state);
@@ -570,7 +562,7 @@
   if (upload_) {
     upload_listener_ = upload_->HasOneOrMoreAttributeEventListener();
   }
-  origin_ = settings_->GetOrigin();
+  origin_ = environment_settings()->GetOrigin();
   // Step 9
   sent_ = true;
   // Now that a send is happening, prevent this object
@@ -798,7 +790,7 @@
 
 scoped_refptr<XMLHttpRequestUpload> XMLHttpRequestImpl::upload() {
   if (!upload_) {
-    upload_ = new XMLHttpRequestUpload(settings_);
+    upload_ = new XMLHttpRequestUpload(environment_settings());
   }
   return upload_;
 }
@@ -900,9 +892,9 @@
   if (fetch_callback_) {
     std::string downloaded_data;
     response_body_->GetAndResetDataAndDownloadProgress(&downloaded_data);
-    script::Handle<script::Uint8Array> data =
-        script::Uint8Array::New(settings_->context()->global_environment(),
-                                downloaded_data.data(), downloaded_data.size());
+    script::Handle<script::Uint8Array> data = script::Uint8Array::New(
+        environment_settings()->context()->global_environment(),
+        downloaded_data.data(), downloaded_data.size());
     fetch_callback_->value().Run(data);
   }
 
@@ -1111,10 +1103,10 @@
 }
 
 web::CspDelegate* XMLHttpRequestImpl::csp_delegate() const {
-  DCHECK(settings_);
-  DCHECK(settings_->context());
-  DCHECK(settings_->context()->GetWindowOrWorkerGlobalScope());
-  return settings_->context()->GetWindowOrWorkerGlobalScope()->csp_delegate();
+  return environment_settings()
+      ->context()
+      ->GetWindowOrWorkerGlobalScope()
+      ->csp_delegate();
 }
 
 void XMLHttpRequestImpl::TerminateRequest() {
@@ -1127,12 +1119,14 @@
     XMLHttpRequest::RequestErrorType request_error_type) {
   // https://www.w3.org/TR/XMLHttpRequest/#timeout-error
   DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
-  DLOG_IF(INFO, verbose())
-      << __FUNCTION__ << " (" << RequestErrorTypeName(request_error_type)
-      << ") " << *xhr_ << std::endl
-      << script::StackTraceToString(
-             settings_->context()->global_environment()->GetStackTrace(
-                 0 /*max_frames*/));
+  DLOG_IF(INFO, verbose()) << __FUNCTION__ << " ("
+                           << RequestErrorTypeName(request_error_type) << ") "
+                           << *xhr_ << std::endl
+                           << script::StackTraceToString(
+                                  environment_settings()
+                                      ->context()
+                                      ->global_environment()
+                                      ->GetStackTrace(0 /*max_frames*/));
   stop_timeout_ = true;
   // Step 1
   TerminateRequest();
@@ -1209,7 +1203,8 @@
         new script::PreallocatedArrayBufferData());
     response_body_->GetAndResetData(downloaded_data.get());
     auto array_buffer = script::ArrayBuffer::New(
-        settings_->context()->global_environment(), std::move(downloaded_data));
+        environment_settings()->context()->global_environment(),
+        std::move(downloaded_data));
     response_array_buffer_reference_.reset(
         new script::ScriptValue<script::ArrayBuffer>::Reference(xhr_,
                                                                 array_buffer));
@@ -1252,7 +1247,7 @@
   response_array_buffer_reference_.reset();
 
   network::NetworkModule* network_module =
-      settings_->context()->fetcher_factory()->network_module();
+      environment_settings()->context()->fetcher_factory()->network_module();
   url_fetcher_ = net::URLFetcher::Create(request_url_, method_, xhr_);
   ++url_fetcher_generation_;
   url_fetcher_->SetRequestContext(network_module->url_request_context_getter());
@@ -1303,7 +1298,8 @@
         origin_.SerializedOrigin(),
         base::Bind(&DOMXMLHttpRequestImpl::CORSPreflightErrorCallback,
                    base::Unretained(this)),
-        settings_->context()
+        environment_settings()
+            ->context()
             ->GetWindowOrWorkerGlobalScope()
             ->get_preflight_cache()));
     corspreflight_->set_headers(request_headers_);
@@ -1326,19 +1322,20 @@
   }
   DLOG_IF(INFO, verbose()) << __FUNCTION__ << *xhr_;
   if (!dopreflight) {
-    DCHECK(settings_->context()->network_module());
-    StartURLFetcher(settings_->context()->network_module()->max_network_delay(),
+    DCHECK(environment_settings()->context()->network_module());
+    StartURLFetcher(environment_settings()
+                        ->context()
+                        ->network_module()
+                        ->max_network_delay(),
                     url_fetcher_generation_);
   }
 }
 
 void XMLHttpRequestImpl::IncrementActiveRequests() {
   if (active_requests_count_ == 0) {
-    DCHECK(settings_);
-    DCHECK(settings_->context());
     prevent_gc_until_send_complete_.reset(
         new script::GlobalEnvironment::ScopedPreventGarbageCollection(
-            settings_->context()->global_environment(), xhr_));
+            environment_settings()->context()->global_environment(), xhr_));
   }
   active_requests_count_++;
 }
@@ -1406,16 +1403,15 @@
 }
 
 void XMLHttpRequestImpl::CORSPreflightSuccessCallback() {
-  DCHECK(settings_->context()->network_module());
-  StartURLFetcher(settings_->context()->network_module()->max_network_delay(),
-                  url_fetcher_generation_);
+  DCHECK(environment_settings()->context()->network_module());
+  StartURLFetcher(
+      environment_settings()->context()->network_module()->max_network_delay(),
+      url_fetcher_generation_);
 }
 
 DOMXMLHttpRequestImpl::DOMXMLHttpRequestImpl(XMLHttpRequest* xhr)
-    : XMLHttpRequestImpl(xhr),
-      settings_(base::polymorphic_downcast<dom::DOMSettings*>(
-          xhr->environment_settings())) {
-  DCHECK(settings_);
+    : XMLHttpRequestImpl(xhr) {
+  DCHECK(environment_settings());
 }
 
 // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#the-responsexml-attribute
@@ -1445,22 +1441,34 @@
 }
 
 void DOMXMLHttpRequestImpl::GetLoadTimingInfoAndCreateResourceTiming() {
-  if (settings_->window()->performance() == nullptr) return;
-  settings_->window()->performance()->CreatePerformanceResourceTiming(
-      load_timing_info_, kPerformanceResourceTimingInitiatorType,
-      request_url_.spec());
+  web::WindowOrWorkerGlobalScope* window_or_worker_global_scope =
+      environment_settings()->context()->GetWindowOrWorkerGlobalScope();
+  if (!window_or_worker_global_scope->IsWindow()) return;
+  if (window_or_worker_global_scope->AsWindow()->performance() == nullptr)
+    return;
+  window_or_worker_global_scope->AsWindow()
+      ->performance()
+      ->CreatePerformanceResourceTiming(load_timing_info_,
+                                        kPerformanceResourceTimingInitiatorType,
+                                        request_url_.spec());
 }
 
 // https://www.w3.org/TR/2014/WD-XMLHttpRequest-20140130/#document-response-entity-body
 scoped_refptr<dom::Document>
 DOMXMLHttpRequestImpl::GetDocumentResponseEntityBody() {
   DCHECK_EQ(state_, XMLHttpRequest::kDone);
+  if (!environment_settings()
+           ->context()
+           ->GetWindowOrWorkerGlobalScope()
+           ->IsWindow()) {
+    return nullptr;
+  }
 
   // Step 1..5
   const std::string final_mime_type =
       mime_type_override_.empty() ? response_mime_type_ : mime_type_override_;
   if (final_mime_type != "text/xml" && final_mime_type != "application/xml") {
-    return NULL;
+    return nullptr;
   }
 
   // 6. Otherwise, let document be a document that represents the result of
@@ -1468,9 +1476,15 @@
   // specifications. If that fails (unsupported character encoding, namespace
   // well-formedness error, etc.), return null.
   scoped_refptr<dom::XMLDocument> xml_document =
-      new dom::XMLDocument(settings_->window()->html_element_context());
+      new dom::XMLDocument(environment_settings()
+                               ->context()
+                               ->GetWindowOrWorkerGlobalScope()
+                               ->AsWindow()
+                               ->html_element_context());
   dom_parser::XMLDecoder xml_decoder(
-      xml_document, xml_document, NULL, settings_->max_dom_element_depth(),
+      xml_document, xml_document, NULL,
+      base::polymorphic_downcast<dom::DOMSettings*>(environment_settings())
+          ->max_dom_element_depth(),
       base::SourceLocation("[object XMLHttpRequest]", 1, 1),
       base::Bind(&DOMXMLHttpRequestImpl::XMLDecoderLoadCompleteCallback,
                  base::Unretained(this)));
diff --git a/cobalt/xhr/xml_http_request.h b/cobalt/xhr/xml_http_request.h
index 01979a6..bbf3591 100644
--- a/cobalt/xhr/xml_http_request.h
+++ b/cobalt/xhr/xml_http_request.h
@@ -310,6 +310,7 @@
  protected:
   void CORSPreflightErrorCallback();
   void CORSPreflightSuccessCallback();
+  web::EnvironmentSettings* environment_settings() const { return settings_; }
 
   // Return the CSP delegate from the Settings object.
   // virtual for use by tests.
@@ -453,7 +454,6 @@
   void XMLDecoderLoadCompleteCallback(
       const base::Optional<std::string>& status);
 
-  dom::DOMSettings* const settings_;
   bool has_xml_decoder_error_;
 };
 
diff --git a/components/prefs/BUILD.gn b/components/prefs/BUILD.gn
index 29f20c7..f29734c 100644
--- a/components/prefs/BUILD.gn
+++ b/components/prefs/BUILD.gn
@@ -26,6 +26,8 @@
     "in_memory_pref_store.h",
     "json_pref_store.cc",
     "json_pref_store.h",
+    "json_read_only_pref_store.cc",
+    "json_read_only_pref_store.h",
     "overlay_user_pref_store.cc",
     "overlay_user_pref_store.h",
     "persistent_pref_store.cc",
diff --git a/components/prefs/json_read_only_pref_store.cc b/components/prefs/json_read_only_pref_store.cc
new file mode 100644
index 0000000..9c50844
--- /dev/null
+++ b/components/prefs/json_read_only_pref_store.cc
@@ -0,0 +1,345 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "components/prefs/json_read_only_pref_store.h"
+
+#include <stddef.h>
+
+#include <algorithm>
+#include <utility>
+
+#include "base/bind.h"
+#include "base/callback.h"
+#include "base/files/file_path.h"
+#include "base/files/file_util.h"
+#include "base/json/json_file_value_serializer.h"
+#include "base/memory/ref_counted.h"
+#include "base/metrics/histogram.h"
+#include "base/sequenced_task_runner.h"
+#include "base/strings/string_number_conversions.h"
+#include "base/strings/string_util.h"
+#include "base/task_runner_util.h"
+#include "base/threading/sequenced_task_runner_handle.h"
+#include "base/values.h"
+#include "components/prefs/pref_filter.h"
+
+// Result returned from internal read tasks.
+struct JsonReadOnlyPrefStore::ReadResult {
+ public:
+  ReadResult();
+  ~ReadResult();
+
+  std::unique_ptr<base::Value> value;
+  PrefReadError error;
+  bool no_dir;
+
+ private:
+  DISALLOW_COPY_AND_ASSIGN(ReadResult);
+};
+
+JsonReadOnlyPrefStore::ReadResult::ReadResult()
+    : error(PersistentPrefStore::PREF_READ_ERROR_NONE), no_dir(false) {}
+
+JsonReadOnlyPrefStore::ReadResult::~ReadResult() {}
+
+namespace {
+
+// Some extensions we'll tack on to copies of the Preferences files.
+const base::FilePath::CharType kBadExtension[] = FILE_PATH_LITERAL("bad");
+
+PersistentPrefStore::PrefReadError HandleReadErrors(
+    const base::Value* value,
+    const base::FilePath& path,
+    int error_code,
+    const std::string& error_msg) {
+  if (!value) {
+    DVLOG(1) << "Error while loading JSON file: " << error_msg
+             << ", file: " << path.value();
+    switch (error_code) {
+      case JSONFileValueDeserializer::JSON_ACCESS_DENIED:
+        return PersistentPrefStore::PREF_READ_ERROR_ACCESS_DENIED;
+      case JSONFileValueDeserializer::JSON_CANNOT_READ_FILE:
+        return PersistentPrefStore::PREF_READ_ERROR_FILE_OTHER;
+      case JSONFileValueDeserializer::JSON_FILE_LOCKED:
+        return PersistentPrefStore::PREF_READ_ERROR_FILE_LOCKED;
+      case JSONFileValueDeserializer::JSON_NO_SUCH_FILE:
+        return PersistentPrefStore::PREF_READ_ERROR_NO_FILE;
+      default:
+        // JSON errors indicate file corruption of some sort.
+        // Since the file is corrupt, move it to the side and continue with
+        // empty preferences.  This will result in them losing their settings.
+        // We keep the old file for possible support and debugging assistance
+        // as well as to detect if they're seeing these errors repeatedly.
+        // TODO(erikkay) Instead, use the last known good file.
+        base::FilePath bad = path.ReplaceExtension(kBadExtension);
+
+        // If they've ever had a parse error before, put them in another bucket.
+        // TODO(erikkay) if we keep this error checking for very long, we may
+        // want to differentiate between recent and long ago errors.
+        bool bad_existed = base::PathExists(bad);
+        base::CopyFile(path, bad);
+        base::DeleteFile(path, false);
+        return bad_existed ? PersistentPrefStore::PREF_READ_ERROR_JSON_REPEAT
+                           : PersistentPrefStore::PREF_READ_ERROR_JSON_PARSE;
+    }
+  }
+  if (!value->is_dict())
+    return PersistentPrefStore::PREF_READ_ERROR_JSON_TYPE;
+  return PersistentPrefStore::PREF_READ_ERROR_NONE;
+}
+
+// Records a sample for |size| in the Settings.JsonDataReadSizeKilobytes
+// histogram suffixed with the base name of the JSON file under |path|.
+void RecordJsonDataSizeHistogram(const base::FilePath& path, size_t size) {
+  std::string spaceless_basename;
+  base::ReplaceChars(path.BaseName().MaybeAsASCII(), " ", "_",
+                     &spaceless_basename);
+
+  // The histogram below is an expansion of the UMA_HISTOGRAM_CUSTOM_COUNTS
+  // macro adapted to allow for a dynamically suffixed histogram name.
+  // Note: The factory creates and owns the histogram.
+  // This histogram is expired but the code was intentionally left behind so
+  // it can be re-enabled on Stable in a single config tweak if needed.
+  base::HistogramBase* histogram = base::Histogram::FactoryGet(
+      "Settings.JsonDataReadSizeKilobytes." + spaceless_basename, 1, 10000, 50,
+      base::HistogramBase::kUmaTargetedHistogramFlag);
+  histogram->Add(static_cast<int>(size) / 1024);
+}
+
+std::unique_ptr<JsonReadOnlyPrefStore::ReadResult> ReadPrefsFromDisk(
+    const base::FilePath& path) {
+  int error_code;
+  std::string error_msg;
+  std::unique_ptr<JsonReadOnlyPrefStore::ReadResult> read_result(
+      new JsonReadOnlyPrefStore::ReadResult);
+  JSONFileValueDeserializer deserializer(path);
+  read_result->value = deserializer.Deserialize(&error_code, &error_msg);
+  read_result->error =
+      HandleReadErrors(read_result->value.get(), path, error_code, error_msg);
+  read_result->no_dir = !base::PathExists(path.DirName());
+
+  if (read_result->error == PersistentPrefStore::PREF_READ_ERROR_NONE)
+    RecordJsonDataSizeHistogram(path, deserializer.get_last_read_size());
+
+  return read_result;
+}
+
+}  // namespace
+
+JsonReadOnlyPrefStore::JsonReadOnlyPrefStore(
+    const base::FilePath& pref_filename,
+    std::unique_ptr<PrefFilter> pref_filter,
+    scoped_refptr<base::SequencedTaskRunner> file_task_runner)
+    : path_(pref_filename),
+      file_task_runner_(std::move(file_task_runner)),
+      prefs_(new base::DictionaryValue()),
+      pref_filter_(std::move(pref_filter)),
+      initialized_(false),
+      filtering_in_progress_(false),
+      read_error_(PREF_READ_ERROR_NONE) {
+  DCHECK(!path_.empty());
+}
+
+bool JsonReadOnlyPrefStore::GetValue(const std::string& key,
+                                     const base::Value** result) const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  base::Value* tmp = nullptr;
+  if (!prefs_->Get(key, &tmp))
+    return false;
+
+  if (result)
+    *result = tmp;
+  return true;
+}
+
+std::unique_ptr<base::DictionaryValue> JsonReadOnlyPrefStore::GetValues()
+    const {
+  return prefs_->CreateDeepCopy();
+}
+
+void JsonReadOnlyPrefStore::AddObserver(PrefStore::Observer* observer) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  observers_.AddObserver(observer);
+}
+
+void JsonReadOnlyPrefStore::RemoveObserver(PrefStore::Observer* observer) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  observers_.RemoveObserver(observer);
+}
+
+bool JsonReadOnlyPrefStore::HasObservers() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  return observers_.might_have_observers();
+}
+
+bool JsonReadOnlyPrefStore::IsInitializationComplete() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  return initialized_;
+}
+
+bool JsonReadOnlyPrefStore::GetMutableValue(const std::string& key,
+                                            base::Value** result) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  return prefs_->Get(key, result);
+}
+
+void JsonReadOnlyPrefStore::SetValue(const std::string& key,
+                                     std::unique_ptr<base::Value> value,
+                                     uint32_t flags) {
+  NOTIMPLEMENTED();
+}
+
+void JsonReadOnlyPrefStore::SetValueSilently(const std::string& key,
+                                             std::unique_ptr<base::Value> value,
+                                             uint32_t flags) {
+  NOTIMPLEMENTED();
+}
+
+void JsonReadOnlyPrefStore::RemoveValue(const std::string& key,
+                                        uint32_t flags) {
+  NOTIMPLEMENTED();
+}
+
+bool JsonReadOnlyPrefStore::ReadOnly() const {
+  return true;
+}
+
+PersistentPrefStore::PrefReadError JsonReadOnlyPrefStore::GetReadError() const {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  return read_error_;
+}
+
+PersistentPrefStore::PrefReadError JsonReadOnlyPrefStore::ReadPrefs() {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  OnFileRead(ReadPrefsFromDisk(path_));
+  return filtering_in_progress_ ? PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE
+                                : read_error_;
+}
+
+void JsonReadOnlyPrefStore::ReadPrefsAsync(ReadErrorDelegate* error_delegate) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  initialized_ = false;
+  error_delegate_.reset(error_delegate);
+
+  // Weakly binds the read task so that it doesn't kick in during shutdown.
+  base::PostTaskAndReplyWithResult(
+      file_task_runner_.get(), FROM_HERE, base::Bind(&ReadPrefsFromDisk, path_),
+      base::Bind(&JsonReadOnlyPrefStore::OnFileRead, AsWeakPtr()));
+}
+
+void JsonReadOnlyPrefStore::CommitPendingWrite(
+    base::OnceClosure reply_callback,
+    base::OnceClosure synchronous_done_callback) {
+  NOTIMPLEMENTED();
+}
+
+void JsonReadOnlyPrefStore::SchedulePendingLossyWrites() {
+  NOTIMPLEMENTED();
+}
+
+void JsonReadOnlyPrefStore::ReportValueChanged(const std::string& key,
+                                               uint32_t flags) {
+  NOTIMPLEMENTED();
+}
+
+void JsonReadOnlyPrefStore::ClearMutableValues() {
+  NOTIMPLEMENTED();
+}
+
+void JsonReadOnlyPrefStore::OnStoreDeletionFromDisk() {
+  if (pref_filter_)
+    pref_filter_->OnStoreDeletionFromDisk();
+}
+
+void JsonReadOnlyPrefStore::OnFileRead(
+    std::unique_ptr<ReadResult> read_result) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  DCHECK(read_result);
+
+  std::unique_ptr<base::DictionaryValue> unfiltered_prefs(
+      new base::DictionaryValue);
+
+  read_error_ = read_result->error;
+
+  bool initialization_successful = !read_result->no_dir;
+
+  if (initialization_successful) {
+    switch (read_error_) {
+      case PREF_READ_ERROR_ACCESS_DENIED:
+      case PREF_READ_ERROR_FILE_OTHER:
+      case PREF_READ_ERROR_FILE_LOCKED:
+      case PREF_READ_ERROR_JSON_TYPE:
+      case PREF_READ_ERROR_FILE_NOT_SPECIFIED:
+        break;
+      case PREF_READ_ERROR_NONE:
+        DCHECK(read_result->value);
+        unfiltered_prefs.reset(
+            static_cast<base::DictionaryValue*>(read_result->value.release()));
+        break;
+      case PREF_READ_ERROR_NO_FILE:
+
+      // If the file just doesn't exist, maybe this is first run.  In any case
+      // there's no harm in writing out default prefs in this case.
+      case PREF_READ_ERROR_JSON_PARSE:
+      case PREF_READ_ERROR_JSON_REPEAT:
+        break;
+      case PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE:
+      // This is a special error code to be returned by ReadPrefs when it
+      // can't complete synchronously, it should never be returned by the read
+      // operation itself.
+      case PREF_READ_ERROR_MAX_ENUM:
+        NOTREACHED();
+        break;
+    }
+  }
+
+  if (pref_filter_) {
+    filtering_in_progress_ = true;
+    const PrefFilter::PostFilterOnLoadCallback post_filter_on_load_callback(
+        base::Bind(&JsonReadOnlyPrefStore::FinalizeFileRead, AsWeakPtr(),
+                   initialization_successful));
+    pref_filter_->FilterOnLoad(post_filter_on_load_callback,
+                               std::move(unfiltered_prefs));
+  } else {
+    FinalizeFileRead(initialization_successful, std::move(unfiltered_prefs),
+                     false);
+  }
+}
+
+void JsonReadOnlyPrefStore::FinalizeFileRead(
+    bool initialization_successful,
+    std::unique_ptr<base::DictionaryValue> prefs,
+    bool schedule_write) {
+  DCHECK_CALLED_ON_VALID_SEQUENCE(sequence_checker_);
+
+  filtering_in_progress_ = false;
+
+  if (!initialization_successful) {
+    for (PrefStore::Observer& observer : observers_)
+      observer.OnInitializationCompleted(false);
+    return;
+  }
+
+  prefs_ = std::move(prefs);
+
+  initialized_ = true;
+
+  if (error_delegate_ && read_error_ != PREF_READ_ERROR_NONE)
+    error_delegate_->OnError(read_error_);
+
+  for (PrefStore::Observer& observer : observers_)
+    observer.OnInitializationCompleted(true);
+
+  return;
+}
diff --git a/components/prefs/json_read_only_pref_store.h b/components/prefs/json_read_only_pref_store.h
new file mode 100644
index 0000000..4577c01
--- /dev/null
+++ b/components/prefs/json_read_only_pref_store.h
@@ -0,0 +1,129 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef COMPONENTS_PREFS_JSON_READ_ONLY_PREF_STORE_H_
+#define COMPONENTS_PREFS_JSON_READ_ONLY_PREF_STORE_H_
+
+#include <stdint.h>
+
+#include <memory>
+#include <set>
+#include <string>
+
+#include "base/callback_forward.h"
+#include "base/compiler_specific.h"
+#include "base/files/file_path.h"
+#include "base/macros.h"
+#include "base/memory/weak_ptr.h"
+#include "base/observer_list.h"
+#include "base/sequence_checker.h"
+#include "base/task/post_task.h"
+#include "components/prefs/persistent_pref_store.h"
+#include "components/prefs/pref_filter.h"
+#include "components/prefs/prefs_export.h"
+
+class PrefFilter;
+
+namespace base {
+class DictionaryValue;
+class FilePath;
+class JsonPrefStoreCallbackTest;
+class SequencedTaskRunner;
+class Value;
+}  // namespace base
+
+// A readonly JSONPrefStore implementation that is used for
+// reading user preferences.
+class COMPONENTS_PREFS_EXPORT JsonReadOnlyPrefStore
+    : public PersistentPrefStore,
+      public base::SupportsWeakPtr<JsonReadOnlyPrefStore> {
+ public:
+  struct ReadResult;
+
+  JsonReadOnlyPrefStore(
+      const base::FilePath& pref_filename,
+      std::unique_ptr<PrefFilter> pref_filter = nullptr,
+      scoped_refptr<base::SequencedTaskRunner> file_task_runner =
+          base::CreateSequencedTaskRunnerWithTraits(
+              {base::MayBlock(), base::TaskPriority::USER_VISIBLE,
+               base::TaskShutdownBehavior::BLOCK_SHUTDOWN}));
+
+  // PrefStore overrides:
+  bool GetValue(const std::string& key,
+                const base::Value** result) const override;
+  std::unique_ptr<base::DictionaryValue> GetValues() const override;
+  void AddObserver(PrefStore::Observer* observer) override;
+  void RemoveObserver(PrefStore::Observer* observer) override;
+  bool HasObservers() const override;
+  bool IsInitializationComplete() const override;
+
+  // PersistentPrefStore overrides:
+  bool GetMutableValue(const std::string& key, base::Value** result) override;
+  void SetValue(const std::string& key,
+                std::unique_ptr<base::Value> value,
+                uint32_t flags) override;
+  void SetValueSilently(const std::string& key,
+                        std::unique_ptr<base::Value> value,
+                        uint32_t flags) override;
+  void RemoveValue(const std::string& key, uint32_t flags) override;
+  bool ReadOnly() const override;
+  PrefReadError GetReadError() const override;
+  // Note this method may be asynchronous if this instance has a |pref_filter_|
+  // in which case it will return PREF_READ_ERROR_ASYNCHRONOUS_TASK_INCOMPLETE.
+  // See details in pref_filter.h.
+  PrefReadError ReadPrefs() override;
+  void ReadPrefsAsync(ReadErrorDelegate* error_delegate) override;
+  void CommitPendingWrite(
+      base::OnceClosure reply_callback = base::OnceClosure(),
+      base::OnceClosure synchronous_done_callback =
+          base::OnceClosure()) override;
+  void SchedulePendingLossyWrites() override;
+  void ReportValueChanged(const std::string& key, uint32_t flags) override;
+
+  void ClearMutableValues() override;
+
+  void OnStoreDeletionFromDisk() override;
+
+ private:
+  friend class base::JsonPrefStoreCallbackTest;
+
+  // This method is called after the JSON file has been read.  It then hands
+  // |value| (or an empty dictionary in some read error cases) to the
+  // |pref_filter| if one is set. It also gives a callback pointing at
+  // FinalizeFileRead() to that |pref_filter_| which is then responsible for
+  // invoking it when done. If there is no |pref_filter_|, FinalizeFileRead()
+  // is invoked directly.
+  void OnFileRead(std::unique_ptr<ReadResult> read_result);
+
+  // This method is called after the JSON file has been read and the result has
+  // potentially been intercepted and modified by |pref_filter_|.
+  // |initialization_successful| is pre-determined by OnFileRead() and should
+  // be used when reporting OnInitializationCompleted().
+  // |schedule_write| indicates whether a write should be immediately scheduled
+  // (typically because the |pref_filter_| has already altered the |prefs|) --
+  // this will be ignored if this store is read-only.
+  void FinalizeFileRead(bool initialization_successful,
+                        std::unique_ptr<base::DictionaryValue> prefs,
+                        bool schedule_write);
+
+  const base::FilePath path_;
+  const scoped_refptr<base::SequencedTaskRunner> file_task_runner_;
+
+  std::unique_ptr<base::DictionaryValue> prefs_;
+
+  std::unique_ptr<PrefFilter> pref_filter_;
+  base::ObserverList<PrefStore::Observer, true>::Unchecked observers_;
+
+  std::unique_ptr<ReadErrorDelegate> error_delegate_;
+
+  bool initialized_;
+  bool filtering_in_progress_;
+  PrefReadError read_error_;
+
+  SEQUENCE_CHECKER(sequence_checker_);
+
+  DISALLOW_COPY_AND_ASSIGN(JsonReadOnlyPrefStore);
+};
+
+#endif  // COMPONENTS_PREFS_JSON_READ_ONLY_PREF_STORE_H_
diff --git a/docker-compose-windows.yml b/docker-compose-windows.yml
new file mode 100644
index 0000000..da83efe
--- /dev/null
+++ b/docker-compose-windows.yml
@@ -0,0 +1,95 @@
+# Copyright 2021 The Cobalt Authors. All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+version: '2.4'
+
+volumes:
+  container-sccache:
+
+x-common-definitions: &common-definitions
+  stdin_open: true
+  environment:
+    - SCCACHE_DIR=c:/root/sccache
+    - SCCACHE_CACHE_SIZE="30G"
+  tty: true
+  volumes:
+    - ${COBALT_SRC:-.}:c:/code/
+    - ${SCCACHE_DIR:-container-sccache}:c:/root/sccache/
+  cpus: ${DOCKER_CPUS:-12}
+  mem_limit: ${DOCKER_MEMLIMIT:-100000M}
+
+services:
+  visual-studio-base:
+    build:
+      context: ./docker/windows/base/visualstudio2017
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=mcr.microsoft.com/windows/servercore:ltsc2019
+    image: visual-studio-base
+    scale: 0
+
+  cobalt-build-win-base:
+    build:
+      context: ./docker/windows/base/build
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=visual-studio-base
+    image: cobalt-build-win-base
+    scale: 0
+    depends_on:
+      - visual-studio-base
+
+  visual-studio-win32-base:
+    build:
+      context: ./docker/windows/base/visualstudio2017
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=mcr.microsoft.com/windows:1809
+    image: visual-studio-win32-base
+    scale: 0
+
+  cobalt-build-win32-base:
+    build:
+      context: ./docker/windows/base/build
+      dockerfile: ./Dockerfile
+      args:
+        - FROM_IMAGE=visual-studio-win32-base
+    image: cobalt-build-win32-base
+    scale: 0
+    depends_on:
+      - visual-studio-win32-base
+
+  win-win32:
+    <<: *common-definitions
+    build:
+      context: ./docker/windows/win32
+      dockerfile: ./Dockerfile
+    depends_on:
+      - cobalt-build-win32-base
+    environment:
+      - PLATFORM=win-win32
+      - CONFIG=${CONFIG:-devel}
+      - TARGET=${TARGET:-cobalt_install}
+      - NINJA_FLAGS
+      - IS_DOCKER=1
+    image: cobalt-build-win32
+
+  build-win-win32:
+    <<: *common-definitions
+    build:
+      context: ./docker/windows/win32
+      dockerfile: ./Dockerfile
+    depends_on:
+      - cobalt-build-win32-base
+    image: cobalt-build-win-win32
diff --git a/docker-compose.yml b/docker-compose.yml
index 08f9df6..d575bc3 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -149,6 +149,16 @@
     image: cobalt-build-docsite-deploy
     entrypoint: ["/code/third_party/repo-publishing-toolkit-local/preview-site.sh", "deploy"]
 
+  build-linux-stub:
+    <<: *build-common-definitions
+    build:
+      context: ./docker/linux
+      dockerfile: stub/Dockerfile
+    image: cobalt-build-linux-stub
+    depends_on: [ build-base ]
+    environment:
+      <<: *shared-build-env
+
   stub:
     <<: *build-common-definitions
     build:
@@ -161,6 +171,16 @@
       PLATFORM: stub
       CONFIG: ${CONFIG:-debug}
 
+  build-linux:
+    <<: *build-common-definitions
+    build:
+      context: ./docker/linux
+      dockerfile: linux-x64x11/Dockerfile
+    image: cobalt-build-linux
+    depends_on: [ build-base ]
+    environment:
+      <<: *shared-build-env
+
   linux-x64x11:
     <<: *build-common-definitions
     build:
@@ -254,6 +274,15 @@
       CONFIG: ${CONFIG:-debug}
       TARGET_CPU: ${TARGET_CPU:-arm64}
 
+  build-raspi:
+    <<: *build-common-definitions
+    build:
+      context: ./docker/linux/raspi
+      dockerfile: ./Dockerfile
+    image: cobalt-build-raspi
+    environment:
+      <<: *shared-build-env
+
   raspi:
     <<: *build-common-definitions
     build:
@@ -371,6 +400,19 @@
       TARGET_CPU: ${TARGET_CPU:-arm}
       SB_API_VERSION: 12
 
+  # Defined common build image for linux-evergreen
+  build-linux-evergreen:
+    <<: *build-common-definitions
+    build:
+      context: ./docker/linux
+      dockerfile: linux-x64x11/Dockerfile
+      args:
+        - FROM_IMAGE=cobalt-build-evergreen
+    image: cobalt-build-linux-evergreen
+    depends_on: [ build-evergreen ]
+    environment:
+      <<: *shared-build-env
+
   linux-x64x11-sbversion12-evergreen:
     <<: *build-common-definitions
     build:
diff --git a/docker/linux/base/build/Dockerfile b/docker/linux/base/build/Dockerfile
index 10098de..aadc720 100644
--- a/docker/linux/base/build/Dockerfile
+++ b/docker/linux/base/build/Dockerfile
@@ -41,9 +41,12 @@
    && echo ${NVM_SHA256SUM} | sha256sum --check \
    && . /tmp/install.sh
 
+# === Pinned Node version to v16, as this is latest one that will work on
+# legacy Debian systems
+ARG LTS_VERSION=v16
 RUN . $NVM_DIR/nvm.sh \
-   && nvm install --lts \
-   && nvm alias default lts/* \
+   && nvm install --lts ${LTS_VERSION} \
+   && nvm alias default ${LTS_VERSION}/* \
    && nvm use default
 
 ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
diff --git a/docker/linux/raspi/Dockerfile b/docker/linux/raspi/Dockerfile
index ad4a8e1..d8f51e8 100644
--- a/docker/linux/raspi/Dockerfile
+++ b/docker/linux/raspi/Dockerfile
@@ -37,7 +37,7 @@
     "https://storage.googleapis.com/cobalt-static-storage/${raspi_tools}" \
     && mkdir -p ${raspi_home} \
     && cd ${raspi_home} \
-    && tar xjvf /tmp/${raspi_tools} \
+    && tar xjvf /tmp/${raspi_tools} --no-same-owner \
     && rm /tmp/${raspi_tools}
 
 CMD gn gen ${OUTDIR}/${PLATFORM}_${CONFIG} --args="target_platform=\"${PLATFORM}\" build_type=\"${CONFIG}\" target_cpu=\"arm\" is_clang=false" && \
diff --git a/docker/linux/unittest/Dockerfile b/docker/linux/unittest/Dockerfile
index b915a10..58f2b96 100644
--- a/docker/linux/unittest/Dockerfile
+++ b/docker/linux/unittest/Dockerfile
@@ -18,21 +18,23 @@
     && apt install -qqy --no-install-recommends \
         libasound2 \
         libatomic1 \
-        libavcodec58 \
-        libavformat58 \
-        libavutil56 \
+        libavcodec-dev \
+        libavformat-dev \
+        libgles2-mesa \
         libegl1-mesa \
         libgl1-mesa-dri \
-        libgles2-mesa \
         libx11-6 \
         libxcomposite1 \
-        libxi6 \
+        libxi-dev \
         libxrender1 \
+        procps \
         unzip \
         xauth \
         xvfb \
     && /opt/clean-after-apt.sh
 
+RUN python3 -m pip install "selenium==3.141.0" "Brotli==1.0.9"
+
 WORKDIR /out
 # Sets the locale in the environment. This is needed for NPLB unit tests.
 ENV LANG en_US.UTF-8
diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.cc b/net/disk_cache/cobalt/cobalt_backend_impl.cc
index 8f58f6d..b60ab38 100644
--- a/net/disk_cache/cobalt/cobalt_backend_impl.cc
+++ b/net/disk_cache/cobalt/cobalt_backend_impl.cc
@@ -96,7 +96,7 @@
     : weak_factory_(this) {
   persistent_settings_ =
       std::make_unique<cobalt::persistent_storage::PersistentSettings>(
-          kPersistentSettingsJson, base::MessageLoop::current()->task_runner());
+          kPersistentSettingsJson);
   ReadDiskCacheSize(persistent_settings_.get(), max_bytes);
 
   // Initialize disk backend for each resource type.
@@ -182,6 +182,11 @@
                                           net::RequestPriority request_priority,
                                           Entry** entry,
                                           CompletionOnceCallback callback) {
+  ResourceType type = GetType(key);
+  auto quota = disk_cache::kTypeMetadata[type].max_size_bytes;
+  if (quota == 0) {
+    return net::Error::ERR_BLOCKED_BY_CLIENT;
+  }
   SimpleBackendImpl* simple_backend = simple_backend_map_[GetType(key)];
   return simple_backend->CreateEntry(key, request_priority, entry,
                                      std::move(callback));
@@ -284,4 +289,10 @@
   return size;
 }
 
+net::Error CobaltBackendImpl::DoomAllEntriesOfType(disk_cache::ResourceType type,
+                        CompletionOnceCallback callback) {
+  SimpleBackendImpl* simple_backend = simple_backend_map_[type];
+  return simple_backend->DoomAllEntries(std::move(callback));
+}
+
 }  // namespace disk_cache
diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.h b/net/disk_cache/cobalt/cobalt_backend_impl.h
index f76d0e9..b3b1b54 100644
--- a/net/disk_cache/cobalt/cobalt_backend_impl.h
+++ b/net/disk_cache/cobalt/cobalt_backend_impl.h
@@ -85,6 +85,8 @@
   size_t DumpMemoryStats(
       base::trace_event::ProcessMemoryDump* pmd,
       const std::string& parent_absolute_name) const override;
+  net::Error DoomAllEntriesOfType(disk_cache::ResourceType type,
+                          CompletionOnceCallback callback);
 
   // A refcounted class that runs a CompletionOnceCallback once it's destroyed.
   class RefCountedRunner : public base::RefCounted<RefCountedRunner> {
diff --git a/net/disk_cache/cobalt/resource_type.h b/net/disk_cache/cobalt/resource_type.h
index 60421ba..6397a9b 100644
--- a/net/disk_cache/cobalt/resource_type.h
+++ b/net/disk_cache/cobalt/resource_type.h
@@ -29,7 +29,8 @@
   kSplashScreen = 5,
   kUncompiledScript = 6,
   kCompiledScript = 7,
-  kTypeCount = 8
+  kCacheApi = 8,
+  kTypeCount = 9
 };
 
 struct ResourceTypeMetadata {
@@ -41,10 +42,11 @@
 // These values are updated on start up in application.cc, using the
 // persisted values saved in settings.json.
 static ResourceTypeMetadata kTypeMetadata[] = {
-    {"other", kInitialBytes},         {"html", kInitialBytes},
-    {"css", kInitialBytes},           {"image", kInitialBytes},
+    {"other", kInitialBytes},         {"html", 2 * 1024 * 1024},
+    {"css", 1 * 1024 * 1024},         {"image", kInitialBytes},
     {"font", kInitialBytes},          {"splash", 2 * 1024 * 1024},
     {"uncompiled_js", kInitialBytes}, {"compiled_js", kInitialBytes},
+    {"cache_api", kInitialBytes},
 };
 
 }  // namespace disk_cache
diff --git a/net/http/http_cache.cc b/net/http/http_cache.cc
index 08346a8..ad07d5b 100644
--- a/net/http/http_cache.cc
+++ b/net/http/http_cache.cc
@@ -585,7 +585,6 @@
   // Strip out the reference, username, and password sections of the URL.
   std::string url = HttpUtil::SpecForRequest(request->url);
 
-  DCHECK_NE(DISABLE, mode_);
   // No valid URL can begin with numerals, so we should not have to worry
   // about collisions with normal URLs.
   if (request->upload_data_stream &&
diff --git a/net/socket/tcp_socket_starboard.cc b/net/socket/tcp_socket_starboard.cc
index 76795d2..c3c6bc5 100644
--- a/net/socket/tcp_socket_starboard.cc
+++ b/net/socket/tcp_socket_starboard.cc
@@ -330,7 +330,6 @@
   peer_address_.reset(new IPEndPoint(address));
 
   SbSocketError result = SbSocketConnect(socket_, &storage);
-  DCHECK_NE(kSbSocketErrorFailed, result);
 
   int rv = MapLastSocketError(socket_);
   if (rv != ERR_IO_PENDING) {
diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
index f36dad3..0b28e08 100644
--- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
+++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java
@@ -78,7 +78,12 @@
   private static final int PLAYBACK_STATE_PLAYING = 0;
   private static final int PLAYBACK_STATE_PAUSED = 1;
   private static final int PLAYBACK_STATE_NONE = 2;
-  private static final String[] PLAYBACK_STATE_NAME = {"playing", "paused", "none"};
+  private static final String[] PLAYBACK_STATE_NAME = {"PLAYING", "PAUSED", "NONE"};
+  private static final int[] PLAYBACK_STATE_COMPAT_MAPPING = {
+    PlaybackStateCompat.STATE_PLAYING,
+    PlaybackStateCompat.STATE_PAUSED,
+    PlaybackStateCompat.STATE_NONE
+  };
 
   // Accessed on the main looper thread only.
   private int currentPlaybackState = PLAYBACK_STATE_NONE;
@@ -205,7 +210,7 @@
     MediaMetadataCompat.Builder metadataBuilder = new MediaMetadataCompat.Builder();
     mediaSession.setMetadata(metadataBuilder.build());
     // |playbackStateBuilder| may still have no fields at this point.
-    mediaSession.setPlaybackState(playbackStateBuilder.build());
+    updatePlaybackState();
   }
 
   private static void checkMainLooperThread() {
@@ -220,10 +225,10 @@
    * are set together to stay coherent as playback state changes. This is idempotent as it may be
    * called multiple times during the course of a media session.
    */
-  private void configureMediaFocus(int playbackState) {
+  private void configureMediaFocus(int playbackState, long positionMs, float speed) {
     checkMainLooperThread();
     if (transientPause && playbackState == PLAYBACK_STATE_PAUSED) {
-      Log.i(TAG, "Media focus: paused (transient)");
+      Log.i(TAG, "Media focus: PAUSED (transient)");
       // Don't release media focus while transiently paused, otherwise we won't get audiofocus back
       // when the transient condition ends and we would leave playback paused.
       return;
@@ -232,6 +237,9 @@
     wakeLock(playbackState == PLAYBACK_STATE_PLAYING);
     audioFocus(playbackState == PLAYBACK_STATE_PLAYING);
 
+    playbackStateBuilder.setState(PLAYBACK_STATE_COMPAT_MAPPING[playbackState], positionMs, speed);
+    updatePlaybackState();
+
     boolean activating = playbackState != PLAYBACK_STATE_NONE;
     boolean deact