summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/test/download_canceled
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/serviceworkers/test/download_canceled/page_download_canceled.html58
-rw-r--r--dom/serviceworkers/test/download_canceled/server-stream-download.sjs133
-rw-r--r--dom/serviceworkers/test/download_canceled/sw_download_canceled.js150
3 files changed, 341 insertions, 0 deletions
diff --git a/dom/serviceworkers/test/download_canceled/page_download_canceled.html b/dom/serviceworkers/test/download_canceled/page_download_canceled.html
new file mode 100644
index 0000000000..e3904c4967
--- /dev/null
+++ b/dom/serviceworkers/test/download_canceled/page_download_canceled.html
@@ -0,0 +1,58 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+
+<script src="../utils.js"></script>
+<script type="text/javascript">
+function wait_until_controlled() {
+ return new Promise(function(resolve) {
+ if (navigator.serviceWorker.controller) {
+ return resolve('controlled');
+ }
+ navigator.serviceWorker.addEventListener('controllerchange', function onController() {
+ if (navigator.serviceWorker.controller) {
+ navigator.serviceWorker.removeEventListener('controllerchange', onController);
+ return resolve('controlled');
+ }
+ });
+ });
+}
+addEventListener('load', async function(event) {
+ window.controlled = wait_until_controlled();
+ window.registration =
+ await navigator.serviceWorker.register('sw_download_canceled.js');
+ let sw = registration.installing || registration.waiting ||
+ registration.active;
+ await waitForState(sw, 'activated');
+ sw.postMessage('claim');
+});
+
+// Place to hold promises for stream closures reported by the SW.
+window.streamClosed = {};
+
+// The ServiceWorker will postMessage to this BroadcastChannel when the streams
+// are closed. (Alternately, the SW could have used the clients API to post at
+// us, but the mechanism by which that operates would be different when this
+// test is uplifted, and it's desirable to avoid timing changes.)
+//
+// The browser test will use this promise to wait on stream shutdown.
+window.swStreamChannel = new BroadcastChannel("stream-closed");
+function trackStreamClosure(path) {
+ let resolve;
+ const promise = new Promise(r => { resolve = r });
+ window.streamClosed[path] = { promise, resolve };
+}
+window.swStreamChannel.onmessage = ({ data }) => {
+ window.streamClosed[data.what].resolve(data);
+};
+</script>
+
+</body>
+</html>
diff --git a/dom/serviceworkers/test/download_canceled/server-stream-download.sjs b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs
new file mode 100644
index 0000000000..f47e872feb
--- /dev/null
+++ b/dom/serviceworkers/test/download_canceled/server-stream-download.sjs
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { setInterval, clearInterval } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+// stolen from file_blocked_script.sjs
+function setGlobalState(data, key) {
+ x = {
+ data,
+ QueryInterface(iid) {
+ return this;
+ },
+ };
+ x.wrappedJSObject = x;
+ setObjectState(key, x);
+}
+
+function getGlobalState(key) {
+ var data;
+ getObjectState(key, function (x) {
+ data = x && x.wrappedJSObject.data;
+ });
+ return data;
+}
+
+/*
+ * We want to let the sw_download_canceled.js service worker know when the
+ * stream was canceled. To this end, we let it issue a monitor request which we
+ * fulfill when the stream has been canceled. In order to coordinate between
+ * multiple requests, we use the getObjectState/setObjectState mechanism that
+ * httpd.js exposes to let data be shared and/or persist between requests. We
+ * handle both possible orderings of the requests because we currently don't
+ * try and impose an ordering between the two requests as issued by the SW, and
+ * file_blocked_script.sjs encourages us to do this, but we probably could order
+ * them.
+ */
+const MONITOR_KEY = "stream-monitor";
+function completeMonitorResponse(response, data) {
+ response.write(JSON.stringify(data));
+ response.finish();
+}
+function handleMonitorRequest(request, response) {
+ response.setHeader("Content-Type", "application/json");
+ response.setStatusLine(request.httpVersion, 200, "Found");
+
+ response.processAsync();
+ // Necessary to cause the headers to be flushed; that or touching the
+ // bodyOutputStream getter.
+ response.write("");
+ dump("server-stream-download.js: monitor headers issued\n");
+
+ const alreadyCompleted = getGlobalState(MONITOR_KEY);
+ if (alreadyCompleted) {
+ completeMonitorResponse(response, alreadyCompleted);
+ setGlobalState(null, MONITOR_KEY);
+ } else {
+ setGlobalState(response, MONITOR_KEY);
+ }
+}
+
+const MAX_TICK_COUNT = 3000;
+const TICK_INTERVAL = 2;
+function handleStreamRequest(request, response) {
+ const name = "server-stream-download";
+
+ // Create some payload to send.
+ let strChunk =
+ "Static routes are the future of ServiceWorkers! So say we all!\n";
+ while (strChunk.length < 1024) {
+ strChunk += strChunk;
+ }
+
+ response.setHeader("Content-Disposition", `attachment; filename="${name}"`);
+ response.setHeader(
+ "Content-Type",
+ `application/octet-stream; name="${name}"`
+ );
+ response.setHeader("Content-Length", `${strChunk.length * MAX_TICK_COUNT}`);
+ response.setStatusLine(request.httpVersion, 200, "Found");
+
+ response.processAsync();
+ response.write(strChunk);
+ dump("server-stream-download.js: stream headers + first payload issued\n");
+
+ let count = 0;
+ let intervalId;
+ function closeStream(why, message) {
+ dump("server-stream-download.js: closing stream: " + why + "\n");
+ clearInterval(intervalId);
+ response.finish();
+
+ const data = { why, message };
+ const monitorResponse = getGlobalState(MONITOR_KEY);
+ if (monitorResponse) {
+ completeMonitorResponse(monitorResponse, data);
+ setGlobalState(null, MONITOR_KEY);
+ } else {
+ setGlobalState(data, MONITOR_KEY);
+ }
+ }
+ function tick() {
+ try {
+ // bound worst-case behavior.
+ if (count++ > MAX_TICK_COUNT) {
+ closeStream("timeout", "timeout");
+ return;
+ }
+ response.write(strChunk);
+ } catch (e) {
+ closeStream("canceled", e.message);
+ }
+ }
+ intervalId = setInterval(tick, TICK_INTERVAL);
+}
+
+Components.utils.importGlobalProperties(["URLSearchParams"]);
+function handleRequest(request, response) {
+ dump(
+ "server-stream-download.js: processing request for " +
+ request.path +
+ "?" +
+ request.queryString +
+ "\n"
+ );
+ const query = new URLSearchParams(request.queryString);
+ if (query.has("monitor")) {
+ handleMonitorRequest(request, response);
+ } else {
+ handleStreamRequest(request, response);
+ }
+}
diff --git a/dom/serviceworkers/test/download_canceled/sw_download_canceled.js b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js
new file mode 100644
index 0000000000..5d9d5f9bfd
--- /dev/null
+++ b/dom/serviceworkers/test/download_canceled/sw_download_canceled.js
@@ -0,0 +1,150 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This file is derived from :bkelly's https://glitch.com/edit/#!/html-sw-stream
+
+addEventListener("install", evt => {
+ evt.waitUntil(self.skipWaiting());
+});
+
+// Create a BroadcastChannel to notify when we have closed our streams.
+const channel = new BroadcastChannel("stream-closed");
+
+const MAX_TICK_COUNT = 3000;
+const TICK_INTERVAL = 4;
+/**
+ * Generate a continuous stream of data at a sufficiently high frequency that a
+ * there"s a good chance of racing channel cancellation.
+ */
+function handleStream(evt, filename) {
+ // Create some payload to send.
+ const encoder = new TextEncoder();
+ let strChunk =
+ "Static routes are the future of ServiceWorkers! So say we all!\n";
+ while (strChunk.length < 1024) {
+ strChunk += strChunk;
+ }
+ const dataChunk = encoder.encode(strChunk);
+
+ evt.waitUntil(
+ new Promise(resolve => {
+ let body = new ReadableStream({
+ start: controller => {
+ const closeStream = why => {
+ console.log("closing stream: " + JSON.stringify(why) + "\n");
+ clearInterval(intervalId);
+ resolve();
+ // In event of error, the controller will automatically have closed.
+ if (why.why != "canceled") {
+ try {
+ controller.close();
+ } catch (ex) {
+ // If we thought we should cancel but experienced a problem,
+ // that's a different kind of failure and we need to report it.
+ // (If we didn't catch the exception here, we'd end up erroneously
+ // in the tick() method's canceled handler.)
+ channel.postMessage({
+ what: filename,
+ why: "close-failure",
+ message: ex.message,
+ ticks: why.ticks,
+ });
+ return;
+ }
+ }
+ // Post prior to performing any attempt to close...
+ channel.postMessage(why);
+ };
+
+ controller.enqueue(dataChunk);
+ let count = 0;
+ let intervalId;
+ function tick() {
+ try {
+ // bound worst-case behavior.
+ if (count++ > MAX_TICK_COUNT) {
+ closeStream({
+ what: filename,
+ why: "timeout",
+ message: "timeout",
+ ticks: count,
+ });
+ return;
+ }
+ controller.enqueue(dataChunk);
+ } catch (e) {
+ closeStream({
+ what: filename,
+ why: "canceled",
+ message: e.message,
+ ticks: count,
+ });
+ }
+ }
+ // Alternately, streams' pull mechanism could be used here, but this
+ // test doesn't so much want to saturate the stream as to make sure the
+ // data is at least flowing a little bit. (Also, the author had some
+ // concern about slowing down the test by overwhelming the event loop
+ // and concern that we might not have sufficent back-pressure plumbed
+ // through and an infinite pipe might make bad things happen.)
+ intervalId = setInterval(tick, TICK_INTERVAL);
+ tick();
+ },
+ });
+ evt.respondWith(
+ new Response(body, {
+ headers: {
+ "Content-Disposition": `attachment; filename="${filename}"`,
+ "Content-Type": "application/octet-stream",
+ },
+ })
+ );
+ })
+ );
+}
+
+/**
+ * Use an .sjs to generate a similar stream of data to the above, passing the
+ * response through directly. Because we're handing off the response but also
+ * want to be able to report when cancellation occurs, we create a second,
+ * overlapping long-poll style fetch that will not finish resolving until the
+ * .sjs experiences closure of its socket and terminates the payload stream.
+ */
+function handlePassThrough(evt, filename) {
+ evt.waitUntil(
+ (async () => {
+ console.log("issuing monitor fetch request");
+ const response = await fetch("server-stream-download.sjs?monitor");
+ console.log("monitor headers received, awaiting body");
+ const data = await response.json();
+ console.log("passthrough monitor fetch completed, notifying.");
+ channel.postMessage({
+ what: filename,
+ why: data.why,
+ message: data.message,
+ });
+ })()
+ );
+ evt.respondWith(
+ fetch("server-stream-download.sjs").then(response => {
+ console.log("server-stream-download.sjs Response received, propagating");
+ return response;
+ })
+ );
+}
+
+addEventListener("fetch", evt => {
+ console.log(`SW processing fetch of ${evt.request.url}`);
+ if (evt.request.url.includes("sw-stream-download")) {
+ return handleStream(evt, "sw-stream-download");
+ }
+ if (evt.request.url.includes("sw-passthrough-download")) {
+ return handlePassThrough(evt, "sw-passthrough-download");
+ }
+});
+
+addEventListener("message", evt => {
+ if (evt.data === "claim") {
+ evt.waitUntil(clients.claim());
+ }
+});