/** * Copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { SnapshotServer } from './snapshotServer'; import { TraceModel } from './traceModel'; // @ts-ignore declare const self: ServiceWorkerGlobalScope; self.addEventListener('install', function(event: any) { self.skipWaiting(); }); self.addEventListener('activate', function(event: any) { event.waitUntil(self.clients.claim()); }); const scopePath = new URL(self.registration.scope).pathname; const loadedTraces = new Map(); async function loadTrace(trace: string, clientId: string, progress: (done: number, total: number) => void): Promise { const entry = loadedTraces.get(trace); if (entry) return entry.traceModel; const traceModel = new TraceModel(); let url = trace.startsWith('http') || trace.startsWith('blob') ? trace : `file?path=${trace}`; // Dropbox does not support cors. if (url.startsWith('https://www.dropbox.com/')) url = 'https://dl.dropboxusercontent.com/' + url.substring('https://www.dropbox.com/'.length); await traceModel.load(url, progress); const snapshotServer = new SnapshotServer(traceModel.storage()); loadedTraces.set(trace, { traceModel, snapshotServer, clientId }); return traceModel; } // @ts-ignore async function doFetch(event: FetchEvent): Promise { const request = event.request; const client = await self.clients.get(event.clientId); const snapshotUrl = request.mode === 'navigate' ? request.url : client!.url; const traceUrl = new URL(snapshotUrl).searchParams.get('trace')!; const { snapshotServer } = loadedTraces.get(traceUrl) || {}; if (request.url.startsWith(self.registration.scope)) { const url = new URL(request.url); const relativePath = url.pathname.substring(scopePath.length - 1); if (relativePath === '/ping') { await gc(); return new Response(null, { status: 200 }); } if (relativePath === '/context') { const traceModel = await loadTrace(traceUrl, event.clientId, (done: number, total: number) => { client.postMessage({ method: 'progress', params: { done, total } }); }); return new Response(JSON.stringify(traceModel!.contextEntry), { status: 200, headers: { 'Content-Type': 'application/json' } }); } if (relativePath.startsWith('/snapshotInfo/')) { if (!snapshotServer) return new Response(null, { status: 404 }); return snapshotServer.serveSnapshotInfo(relativePath, url.searchParams); } if (relativePath.startsWith('/snapshot/')) { if (!snapshotServer) return new Response(null, { status: 404 }); return snapshotServer.serveSnapshot(relativePath, url.searchParams, snapshotUrl); } if (relativePath.startsWith('/sha1/')) { // Sha1 is unique, load it from either of the models for simplicity. for (const { traceModel } of loadedTraces.values()) { const blob = await traceModel!.resourceForSha1(relativePath.slice('/sha1/'.length)); if (blob) return new Response(blob, { status: 200 }); } return new Response(null, { status: 404 }); } // Fallback to network. return fetch(event.request); } if (!snapshotServer) return new Response(null, { status: 404 }); return snapshotServer.serveResource(request.url, snapshotUrl); } async function gc() { const usedTraces = new Set(); for (const [traceUrl, entry] of loadedTraces) { const client = await self.clients.get(entry.clientId); if (client) usedTraces.add(traceUrl); } for (const traceUrl of loadedTraces.keys()) { if (!usedTraces.has(traceUrl)) loadedTraces.delete(traceUrl); } } // @ts-ignore self.addEventListener('fetch', function(event: FetchEvent) { event.respondWith(doFetch(event)); });