summaryrefslogtreecommitdiffstats
path: root/dom/serviceworkers/test/download_canceled/sw_download_canceled.js
blob: 5d9d5f9bfd2d3bc9320f87b03079e28528edd3da (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
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());
  }
});