sw_inter_sw_postmessage.js (6074B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 let bc = new BroadcastChannel("inter-sw-postmessage"); 5 let myId = /\/sw-(.)+$/.exec(registration.scope)[1]; 6 // If we are being imported by the generated script from 7 // `sw_always_updating_inter_sw_postmessage.sjs`, there will be a "version" 8 // global and it starts counting from 1. 9 let myVersion = "version" in globalThis ? globalThis.version : 0; 10 let myFullId = `${myId}#${myVersion}`; 11 12 onactivate = function () { 13 bc.postMessage(`${myId}:version-activated:${myVersion}`); 14 }; 15 16 function extractId(urlStr) { 17 if (!urlStr) { 18 return urlStr; 19 } 20 const qIndex = urlStr.indexOf("?"); 21 if (qIndex >= 0) { 22 return urlStr.substring(qIndex + 1); 23 } 24 if (urlStr.endsWith("/empty_with_utils.html")) { 25 return "helper"; 26 } 27 return urlStr; 28 } 29 30 function describeSource(source) { 31 // Note that WindowProxy is impossible here, so we don't check it. 32 if (source === null) { 33 return "null"; 34 } else if (source instanceof MessagePort) { 35 return "port"; 36 } else if (source instanceof WindowClient) { 37 return `wc-${extractId(source.url)}`; 38 } else if (source instanceof Client) { 39 return `c-${extractId(source.url)}`; 40 } else if (source instanceof ServiceWorker) { 41 return `sw-${extractId(source.scriptURL)}`; 42 } else { 43 return "unexpected"; 44 } 45 } 46 47 let lastPostMessageSource = null; 48 globalThis.onmessage = async function handle_message(evt) { 49 console.log(myId, "received postMessage"); 50 lastPostMessageSource = evt.source; 51 bc.postMessage( 52 `${myId}:received-post-message-from:${describeSource(evt.source)}` 53 ); 54 }; 55 56 /** 57 * Map a target descriptor onto something we can postMessage. Possible options 58 * and the resulting target: 59 * - `last-source`: The `.source` property of the most recent event received via 60 * `globalThis.onmessage`. 61 * - `reg-sw-ID`: The active ServiceWorker found on a registration whose 62 * scriptURL ends with `?ID`. This allows us to distinguish between multiple 63 * (non-self-updating) ServiceWorkers on the same registration because each SW 64 * can be given a distinct script path via the `?ID` suffix. But it does not 65 * work for self-updating ServiceWorkers where the only difference is the 66 * version identifier embedded in the script itself. 67 */ 68 async function resolveTarget(descriptor) { 69 if (descriptor === "last-source") { 70 return lastPostMessageSource; 71 } else if (descriptor.startsWith("reg-")) { 72 const registrations = await navigator.serviceWorker.getRegistrations(); 73 let filterFunc; 74 if (descriptor.startsWith("reg-sw-")) { 75 const descriptorId = /^reg-sw-(.+)$/.exec(descriptor)[1]; 76 console.log( 77 "Looking for registration with id", 78 descriptorId, 79 "across", 80 registrations.length, 81 "registrations" 82 ); 83 filterFunc = sw => { 84 if (sw) { 85 console.log("checking SW", sw.scriptURL); 86 } 87 return extractId(sw?.scriptURL) === descriptorId; 88 }; 89 } else { 90 throw new Error(`Target selector '${descriptor}' not understood`); 91 } 92 93 for (const reg of registrations) { 94 console.log("Reg scriptURL", reg.active?.scriptURL); 95 if (filterFunc(reg.active)) { 96 return reg.active; 97 } else if (filterFunc(reg.waiting)) { 98 return reg.waiting; 99 } else if (filterFunc(reg.installing)) { 100 return reg.installing; 101 } 102 } 103 throw new Error("No registration matches found!"); 104 } 105 throw new Error(`Target selector '${descriptor}' not understood`); 106 } 107 108 /** 109 * Map a registration descriptor onto a registration. Options: 110 * - `scope-ID`: The registration with a scope ending with `/sw-ID`. 111 */ 112 async function resolveRegistration(descriptor) { 113 if (descriptor.startsWith("sw-")) { 114 const registrations = await navigator.serviceWorker.getRegistrations(); 115 116 const scopeSuffix = `/${descriptor}`; 117 for (const reg of registrations) { 118 if (reg.scope.endsWith(scopeSuffix)) { 119 return reg; 120 } 121 } 122 123 throw new Error("No registration matches found!"); 124 } 125 throw new Error(`Registration selector '${descriptor}' not understood`); 126 } 127 128 bc.onmessage = async function handle_bc(evt) { 129 // Split the message into colon-delimited commands of the form: 130 // <who should do the thing>:<the command>:<the target of the command> 131 if (typeof evt?.data !== "string") { 132 return; 133 } 134 const pieces = evt?.data?.split(":"); 135 if ( 136 !pieces || 137 pieces.length < 2 || 138 (pieces[0] !== myId && pieces[0] !== myFullId) 139 ) { 140 return; 141 } 142 143 const cmd = pieces[1]; 144 try { 145 if (cmd === "post-message-to") { 146 const target = await resolveTarget(pieces[2]); 147 target.postMessage("yo!"); 148 } else if (cmd === "update-reg") { 149 const reg = await resolveRegistration(pieces[2]); 150 reg.update(); 151 } else if (cmd === "install-reg") { 152 const installId = pieces[2]; 153 const scope = `sw-${installId}`; 154 const script = `sw_inter_sw_postmessage.js?${installId}`; 155 await navigator.serviceWorker.register(script, { 156 scope, 157 }); 158 bc.postMessage(`${myId}:registered:${installId}`); 159 } else if (cmd === "workerref-hang") { 160 const topic = pieces[2]; 161 globalThis.WorkerTestUtils.holdStrongWorkerRefUntilMainThreadObserverNotified( 162 topic 163 ); 164 bc.postMessage(`${myId}:workerref-hung:${topic}`); 165 } else if (cmd === "block") { 166 const topic = pieces[2]; 167 globalThis.WorkerTestUtils.blockUntilMainThreadObserverNotified( 168 topic, 169 // This callback is invoked once the observer has been registered. 170 () => { 171 bc.postMessage(`${myId}:blocking:${topic}`); 172 } 173 ); 174 } else if (cmd === "notify-observer") { 175 const topic = pieces[2]; 176 globalThis.WorkerTestUtils.notifyObserverOnMainThread(topic); 177 bc.postMessage(`${myId}:notified-observer:${topic}`); 178 } 179 } catch (ex) { 180 console.error(ex); 181 bc.postMessage({ 182 error: ex + "", 183 myId, 184 processing: evt?.data, 185 }); 186 } 187 };