sw_download_canceled.js (5182B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 // This file is derived from :bkelly's https://glitch.com/edit/#!/html-sw-stream 5 6 addEventListener("install", evt => { 7 evt.waitUntil(self.skipWaiting()); 8 }); 9 10 // Create a BroadcastChannel to notify when we have closed our streams. 11 const channel = new BroadcastChannel("stream-closed"); 12 13 const MAX_TICK_COUNT = 3000; 14 const TICK_INTERVAL = 4; 15 /** 16 * Generate a continuous stream of data at a sufficiently high frequency that a 17 * there"s a good chance of racing channel cancellation. 18 */ 19 function handleStream(evt, filename) { 20 // Create some payload to send. 21 const encoder = new TextEncoder(); 22 let strChunk = 23 "Static routes are the future of ServiceWorkers! So say we all!\n"; 24 while (strChunk.length < 1024) { 25 strChunk += strChunk; 26 } 27 const dataChunk = encoder.encode(strChunk); 28 29 evt.waitUntil( 30 new Promise(resolve => { 31 let body = new ReadableStream({ 32 start: controller => { 33 const closeStream = why => { 34 console.log("closing stream: " + JSON.stringify(why) + "\n"); 35 clearInterval(intervalId); 36 resolve(); 37 // In event of error, the controller will automatically have closed. 38 if (why.why != "canceled") { 39 try { 40 controller.close(); 41 } catch (ex) { 42 // If we thought we should cancel but experienced a problem, 43 // that's a different kind of failure and we need to report it. 44 // (If we didn't catch the exception here, we'd end up erroneously 45 // in the tick() method's canceled handler.) 46 channel.postMessage({ 47 what: filename, 48 why: "close-failure", 49 message: ex.message, 50 ticks: why.ticks, 51 }); 52 return; 53 } 54 } 55 // Post prior to performing any attempt to close... 56 channel.postMessage(why); 57 }; 58 59 controller.enqueue(dataChunk); 60 let count = 0; 61 let intervalId; 62 function tick() { 63 try { 64 // bound worst-case behavior. 65 if (count++ > MAX_TICK_COUNT) { 66 closeStream({ 67 what: filename, 68 why: "timeout", 69 message: "timeout", 70 ticks: count, 71 }); 72 return; 73 } 74 controller.enqueue(dataChunk); 75 } catch (e) { 76 closeStream({ 77 what: filename, 78 why: "canceled", 79 message: e.message, 80 ticks: count, 81 }); 82 } 83 } 84 // Alternately, streams' pull mechanism could be used here, but this 85 // test doesn't so much want to saturate the stream as to make sure the 86 // data is at least flowing a little bit. (Also, the author had some 87 // concern about slowing down the test by overwhelming the event loop 88 // and concern that we might not have sufficent back-pressure plumbed 89 // through and an infinite pipe might make bad things happen.) 90 intervalId = setInterval(tick, TICK_INTERVAL); 91 tick(); 92 }, 93 }); 94 evt.respondWith( 95 new Response(body, { 96 headers: { 97 "Content-Disposition": `attachment; filename="${filename}"`, 98 "Content-Type": "application/octet-stream", 99 }, 100 }) 101 ); 102 }) 103 ); 104 } 105 106 /** 107 * Use an .sjs to generate a similar stream of data to the above, passing the 108 * response through directly. Because we're handing off the response but also 109 * want to be able to report when cancellation occurs, we create a second, 110 * overlapping long-poll style fetch that will not finish resolving until the 111 * .sjs experiences closure of its socket and terminates the payload stream. 112 */ 113 function handlePassThrough(evt, filename) { 114 evt.waitUntil( 115 (async () => { 116 console.log("issuing monitor fetch request"); 117 const response = await fetch("server-stream-download.sjs?monitor"); 118 console.log("monitor headers received, awaiting body"); 119 const data = await response.json(); 120 console.log("passthrough monitor fetch completed, notifying."); 121 channel.postMessage({ 122 what: filename, 123 why: data.why, 124 message: data.message, 125 }); 126 })() 127 ); 128 evt.respondWith( 129 fetch("server-stream-download.sjs").then(response => { 130 console.log("server-stream-download.sjs Response received, propagating"); 131 return response; 132 }) 133 ); 134 } 135 136 addEventListener("fetch", evt => { 137 console.log(`SW processing fetch of ${evt.request.url}`); 138 if (evt.request.url.includes("sw-stream-download")) { 139 handleStream(evt, "sw-stream-download"); 140 return; 141 } 142 if (evt.request.url.includes("sw-passthrough-download")) { 143 handlePassThrough(evt, "sw-passthrough-download"); 144 } 145 }); 146 147 addEventListener("message", evt => { 148 if (evt.data === "claim") { 149 evt.waitUntil(clients.claim()); 150 } 151 });