1const GC_BROWSER_PUSH_SW_VERSION = "2026-05-29" 2const GC_BROWSER_PUSH_FALLBACK_PATH = "/notifications" 3const GC_BROWSER_PUSH_FALLBACK_TITLE = "Gamer Center" 4const GC_BROWSER_PUSH_FALLBACK_BODY = "You have a new notification" 5 6self.addEventListener("install", (event) => { 7 console.info("[GC_BROWSER_PUSH_SW_INSTALL]", { 8 version: GC_BROWSER_PUSH_SW_VERSION, 9 }) 10 event.waitUntil(self.skipWaiting()) 11}) 12 13self.addEventListener("activate", (event) => { 14 console.info("[GC_BROWSER_PUSH_SW_ACTIVATE]", { 15 version: GC_BROWSER_PUSH_SW_VERSION, 16 }) 17 event.waitUntil(self.clients.claim()) 18}) 19 20self.addEventListener("push", (event) => { 21 const notification = parsePushNotification(event) 22 23 event.waitUntil( 24 self.registration.showNotification(notification.title, notification.options) 25 ) 26}) 27 28self.addEventListener("notificationclick", (event) => { 29 event.notification.close() 30 31 const path = validateNotificationPath( 32 getString(event.notification.data, "path") || 33 getString(event.notification.data, "url") 34 ) 35 36 event.waitUntil(openNotificationPath(path)) 37}) 38 39function parsePushNotification(event) { 40 const payload = parsePushPayload(event) 41 const data = getRecord(payload, "data") 42 const notificationId = 43 getString(payload, "notificationId") || getString(data, "notificationId") 44 const path = validateNotificationPath( 45 getString(payload, "path") || 46 getString(payload, "url") || 47 getString(data, "path") || 48 getString(data, "url") 49 ) 50 const tag = 51 getString(payload, "tag") || 52 (notificationId ? `gc-notification:${notificationId}` : undefined) 53 54 return { 55 title: getString(payload, "title") || GC_BROWSER_PUSH_FALLBACK_TITLE, 56 options: { 57 badge: validateOptionalNotificationAssetPath(getString(payload, "badge")), 58 body: getString(payload, "body") || GC_BROWSER_PUSH_FALLBACK_BODY, 59 data: { 60 notificationId, 61 path, 62 }, 63 icon: validateOptionalNotificationAssetPath(getString(payload, "icon")), 64 renotify: getBoolean(payload, "renotify"), 65 tag, 66 }, 67 } 68} 69 70function parsePushPayload(event) { 71 if (!event.data) { 72 return {} 73 } 74 75 try { 76 const payload = event.data.json() 77 if (isRecord(payload)) { 78 return payload 79 } 80 } catch (error) { 81 console.warn("[GC_BROWSER_PUSH_SW_PAYLOAD_PARSE_ERROR]", { 82 version: GC_BROWSER_PUSH_SW_VERSION, 83 }) 84 } 85 86 return {} 87} 88 89async function openNotificationPath(path) { 90 const windowClients = await self.clients.matchAll({ 91 includeUncontrolled: true, 92 type: "window", 93 }) 94 95 for (const client of windowClients) { 96 const clientUrl = new URL(client.url) 97 const clientPath = `${clientUrl.pathname}${clientUrl.search}${clientUrl.hash}` 98 99 if (clientUrl.origin === self.location.origin && clientPath === path) { 100 return client.focus() 101 } 102 } 103 104 for (const client of windowClients) { 105 const clientUrl = new URL(client.url) 106 107 if (clientUrl.origin === self.location.origin && "navigate" in client) { 108 const navigatedClient = await client.navigate(path) 109 110 if (navigatedClient) { 111 return navigatedClient.focus() 112 } 113 } 114 } 115 116 return self.clients.openWindow(path) 117} 118 119function validateNotificationPath(value) { 120 if (typeof value !== "string") { 121 return GC_BROWSER_PUSH_FALLBACK_PATH 122 } 123 124 const path = value.trim() 125 126 if (!path.startsWith("/")) { 127 return GC_BROWSER_PUSH_FALLBACK_PATH 128 } 129 130 if (path.startsWith("//")) { 131 return GC_BROWSER_PUSH_FALLBACK_PATH 132 } 133 134 if (/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(path)) { 135 return GC_BROWSER_PUSH_FALLBACK_PATH 136 } 137 138 return path 139} 140 141function validateOptionalNotificationAssetPath(value) { 142 if (typeof value !== "string") { 143 return undefined 144 } 145 146 const path = validateNotificationPath(value) 147 148 if (path === GC_BROWSER_PUSH_FALLBACK_PATH && value.trim() !== path) { 149 return undefined 150 } 151 152 return path 153} 154 155function getRecord(value, key) { 156 if (!isRecord(value)) { 157 return {} 158 } 159 160 const nested = value[key] 161 162 if (!isRecord(nested)) { 163 return {} 164 } 165 166 return nested 167} 168 169function getString(value, key) { 170 if (!isRecord(value)) { 171 return undefined 172 } 173 174 const nextValue = value[key] 175 176 if (typeof nextValue !== "string") { 177 return undefined 178 } 179 180 return nextValue 181} 182 183function getBoolean(value, key) { 184 if (!isRecord(value)) { 185 return undefined 186 } 187 188 const nextValue = value[key] 189 190 if (typeof nextValue !== "boolean") { 191 return undefined 192 } 193 194 return nextValue 195} 196 197function isRecord(value) { 198 return typeof value === "object" && value !== null 199}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.