1414 * limitations under the License. 
1515 */ 
1616
17- import  {  splitProgress  }  from  './progress' ; 
17+ import  {  Progress ,   splitProgress  }  from  './progress' ; 
1818import  {  SnapshotServer  }  from  './snapshotServer' ; 
1919import  {  TraceModel  }  from  './traceModel' ; 
2020import  {  FetchTraceModelBackend ,  traceFileURL ,  ZipTraceModelBackend  }  from  './traceModelBackends' ; 
@@ -41,6 +41,13 @@ type ServiceWorkerGlobalScope = {
4141  skipWaiting ( ) : Promise < void > ; 
4242} ; 
4343
44+ type  FetchEvent  =  { 
45+   request : Request ; 
46+   clientId : string  |  null ; 
47+   resultingClientId : string ; 
48+   respondWith ( response : Promise < Response > ) : void ; 
49+ } ; 
50+ 
4451declare  const  self : ServiceWorkerGlobalScope ; 
4552
4653self . addEventListener ( 'install' ,  function ( event : any )  { 
@@ -57,145 +64,178 @@ type LoadedTrace = {
5764} ; 
5865
5966const  scopePath  =  new  URL ( self . registration . scope ) . pathname ; 
60- const  loadedTraces  =  new  Map < string ,  LoadedTrace > ( ) ; 
67+ const  loadedTraces  =  new  Map < string ,  Promise < LoadedTrace > > ( ) ; 
6168const  clientIdToTraceUrls  =  new  Map < string ,  string > ( ) ; 
6269const  isDeployedAsHttps  =  self . registration . scope . startsWith ( 'https://' ) ; 
6370
64- async  function  loadTrace ( traceUrl : string ,  traceFileName : string  |  null ,  client : Client ) : Promise < TraceModel >  { 
65-   const  clientId  =  client . id ; 
71+ function  simulateRestart ( )  { 
72+   loadedTraces . clear ( ) ; 
73+   clientIdToTraceUrls . clear ( ) ; 
74+ } 
75+ 
76+ async  function  loadTraceOrError ( clientId : string ,  url : URL ,  isContextRequest : boolean ,  progress : Progress ) : Promise < {  loadedTrace ?: LoadedTrace ,  errorResponse ?: Response  } >  { 
77+   try  { 
78+     const  loadedTrace  =  await  loadTrace ( clientId ,  url ,  isContextRequest ,  progress ) ; 
79+     return  {  loadedTrace } ; 
80+   }  catch  ( error )  { 
81+     return  { 
82+       errorResponse : new  Response ( JSON . stringify ( {  error : error ?. message  } ) ,  { 
83+         status : 500 , 
84+         headers : {  'Content-Type' : 'application/json'  } 
85+       } ) 
86+     } ; 
87+   } 
88+ } 
89+ 
90+ function  loadTrace ( clientId : string ,  url : URL ,  isContextRequest : boolean ,  progress : Progress ) : Promise < LoadedTrace >  { 
91+   const  traceUrl  =  url . searchParams . get ( 'trace' ) ! ; 
92+   if  ( ! traceUrl ) 
93+     throw  new  Error ( 'trace parameter is missing' ) ; 
94+ 
6695  clientIdToTraceUrls . set ( clientId ,  traceUrl ) ; 
96+   const  omitCache  =  isContextRequest  &&  isLiveTrace ( traceUrl ) ; 
97+   const  loadedTrace  =  omitCache  ? undefined  : loadedTraces . get ( traceUrl ) ; 
98+   if  ( loadedTrace ) 
99+     return  loadedTrace ; 
100+   const  promise  =  innerLoadTrace ( traceUrl ,  progress ) ; 
101+   loadedTraces . set ( traceUrl ,  promise ) ; 
102+   return  promise ; 
103+ } 
104+ 
105+ async  function  innerLoadTrace ( traceUrl : string ,  progress : Progress ) : Promise < LoadedTrace >  { 
67106  await  gc ( ) ; 
68107
69108  const  traceModel  =  new  TraceModel ( ) ; 
70109  try  { 
71110    // Allow 10% to hop from sw to page. 
72-     const  [ fetchProgress ,  unzipProgress ]  =  splitProgress ( ( done : number ,  total : number )  =>  { 
73-       client . postMessage ( {  method : 'progress' ,  params : {  done,  total }  } ) ; 
74-     } ,  [ 0.5 ,  0.4 ,  0.1 ] ) ; 
75-     const  backend  =  traceUrl . endsWith ( 'json' )  ? new  FetchTraceModelBackend ( traceUrl )  : new  ZipTraceModelBackend ( traceUrl ,  fetchProgress ) ; 
111+     const  [ fetchProgress ,  unzipProgress ]  =  splitProgress ( progress ,  [ 0.5 ,  0.4 ,  0.1 ] ) ; 
112+     const  backend  =  isLiveTrace ( traceUrl )  ? new  FetchTraceModelBackend ( traceUrl )  : new  ZipTraceModelBackend ( traceUrl ,  fetchProgress ) ; 
76113    await  traceModel . load ( backend ,  unzipProgress ) ; 
77114  }  catch  ( error : any )  { 
78115    // eslint-disable-next-line no-console 
79116    console . error ( error ) ; 
80117    if  ( error ?. message ?. includes ( 'Cannot find .trace file' )  &&  await  traceModel . hasEntry ( 'index.html' ) ) 
81118      throw  new  Error ( 'Could not load trace. Did you upload a Playwright HTML report instead? Make sure to extract the archive first and then double-click the index.html file or put it on a web server.' ) ; 
82119    if  ( error  instanceof  TraceVersionError ) 
83-       throw  new  Error ( `Could not load trace from ${ traceFileName  ||  traceUrl }  . ${ error . message }  ` ) ; 
84-     if  ( traceFileName ) 
85-       throw  new  Error ( `Could not load trace from ${ traceFileName }  . Make sure to upload a valid Playwright trace.` ) ; 
120+       throw  new  Error ( `Could not load trace from ${ traceUrl }  . ${ error . message }  ` ) ; 
86121    throw  new  Error ( `Could not load trace from ${ traceUrl }  . Make sure a valid Playwright Trace is accessible over this url.` ) ; 
87122  } 
88123  const  snapshotServer  =  new  SnapshotServer ( traceModel . storage ( ) ,  sha1  =>  traceModel . resourceForSha1 ( sha1 ) ) ; 
89-   loadedTraces . set ( traceUrl ,  {  traceModel,  snapshotServer } ) ; 
90-   return  traceModel ; 
124+   return  {  traceModel,  snapshotServer } ; 
91125} 
92126
93- // @ts -ignore 
94127async  function  doFetch ( event : FetchEvent ) : Promise < Response >  { 
128+   const  request  =  event . request ; 
129+ 
95130  // In order to make Accessibility Insights for Web work. 
96-   if  ( event . request . url . startsWith ( 'chrome-extension://' ) ) 
97-     return  fetch ( event . request ) ; 
131+   if  ( request . url . startsWith ( 'chrome-extension://' ) ) 
132+     return  fetch ( request ) ; 
98133
99-   if  ( event . request . headers . get ( 'x-pw-serviceworker' )  ===  'forward' )  { 
134+   if  ( request . headers . get ( 'x-pw-serviceworker' )  ===  'forward' )  { 
100135    const  request  =  new  Request ( event . request ) ; 
101136    request . headers . delete ( 'x-pw-serviceworker' ) ; 
102137    return  fetch ( request ) ; 
103138  } 
104139
105-   const  request  =  event . request ; 
106-   const  client  =  await  self . clients . get ( event . clientId )  as  Client  |  undefined ; 
107- 
108-   // When trace viewer is deployed over https, we will force upgrade 
109-   // insecure http subresources to https. Otherwise, these will fail 
110-   // to load inside our https snapshots. 
111-   // In this case, we also match http resources from the archive by 
112-   // the https urls. 
113140  const  url  =  new  URL ( request . url ) ; 
114- 
115141  let  relativePath : string  |  undefined ; 
116142  if  ( request . url . startsWith ( self . registration . scope ) ) 
117143    relativePath  =  url . pathname . substring ( scopePath . length  -  1 ) ; 
118144
145+   if  ( relativePath  ===  '/restartServiceWorker' )  { 
146+     simulateRestart ( ) ; 
147+     return  new  Response ( null ,  {  status : 200  } ) ; 
148+   } 
149+ 
119150  if  ( relativePath  ===  '/ping' ) 
120151    return  new  Response ( null ,  {  status : 200  } ) ; 
121152
122-   if  ( relativePath  ===  '/contexts' )  { 
123-     const  traceUrl  =  url . searchParams . get ( 'trace' ) ; 
124-     if  ( ! client  ||  ! traceUrl )  { 
125-       return  new  Response ( 'Something went wrong, trace is requested as a part of the navigation' ,  { 
126-         status : 500 , 
127-         headers : {  'Content-Type' : 'application/json'  } 
128-       } ) ; 
153+   const  isNavigation  =  ! ! event . resultingClientId ; 
154+   const  client  =  event . clientId  ? await  self . clients . get ( event . clientId )  : undefined ; 
155+ 
156+   if  ( isNavigation  &&  ! relativePath ?. startsWith ( '/sha1/' ) )  { 
157+     // Navigation request. Download is a /sha1/ navigation, ignore them here. 
158+ 
159+     // Snapshot iframe navigation request. 
160+     if  ( relativePath ?. startsWith ( '/snapshot/' ) )  { 
161+       // It is Ok to pass noop progress as the trace is likely already loaded. 
162+       const  {  errorResponse,  loadedTrace }  =  await  loadTraceOrError ( event . resultingClientId ! ,  url ,  false ,  noopProgress ) ; 
163+       if  ( errorResponse ) 
164+         return  errorResponse ; 
165+       const  pageOrFrameId  =  relativePath . substring ( '/snapshot/' . length ) ; 
166+       const  response  =  loadedTrace ! . snapshotServer . serveSnapshot ( pageOrFrameId ,  url . searchParams ,  url . href ) ; 
167+       if  ( isDeployedAsHttps ) 
168+         response . headers . set ( 'Content-Security-Policy' ,  'upgrade-insecure-requests' ) ; 
169+       return  response ; 
129170    } 
130171
131-     try  { 
132-       const  traceModel  =  await  loadTrace ( traceUrl ,  url . searchParams . get ( 'traceFileName' ) ,  client ) ; 
133-       return  new  Response ( JSON . stringify ( traceModel . contextEntries ) ,  { 
134-         status : 200 , 
135-         headers : {  'Content-Type' : 'application/json'  } 
136-       } ) ; 
137-     }  catch  ( error : any )  { 
138-       return  new  Response ( JSON . stringify ( {  error : error ?. message  } ) ,  { 
139-         status : 500 , 
140-         headers : {  'Content-Type' : 'application/json'  } 
141-       } ) ; 
142-     } 
172+     // Static content navigation request for trace viewer or popout. 
173+     return  fetch ( event . request ) ; 
143174  } 
144175
145-   if  ( relativePath ?. startsWith ( '/snapshotInfo/' ) )  { 
146-     const  {  snapshotServer }  =  loadedTrace ( url ) ; 
147-     if  ( ! snapshotServer ) 
148-       return  new  Response ( null ,  {  status : 404  } ) ; 
149-     const  pageOrFrameId  =  relativePath . substring ( '/snapshotInfo/' . length ) ; 
150-     return  snapshotServer . serveSnapshotInfo ( pageOrFrameId ,  url . searchParams ) ; 
151-   } 
176+   if  ( ! relativePath )  { 
177+     // Out-of-scope sub-resource request => iframe snapshot sub-resources. 
178+     if  ( ! client ) 
179+       return  new  Response ( 'Sub-resource without a client' ,  {  status : 500  } ) ; 
152180
153-   if  ( relativePath ?. startsWith ( '/snapshot/' ) )  { 
154-     const  {  snapshotServer }  =  loadedTrace ( url ) ; 
181+     const  {  snapshotServer }  =  await  loadTrace ( client . id ,  new  URL ( client . url ) ,  false ,  clientProgress ( client ) ) ; 
155182    if  ( ! snapshotServer ) 
156183      return  new  Response ( null ,  {  status : 404  } ) ; 
157-     const  pageOrFrameId  =  relativePath . substring ( '/snapshot/' . length ) ; 
158-     const  response  =  snapshotServer . serveSnapshot ( pageOrFrameId ,  url . searchParams ,  url . href ) ; 
159-     if  ( isDeployedAsHttps ) 
160-       response . headers . set ( 'Content-Security-Policy' ,  'upgrade-insecure-requests' ) ; 
161-     return  response ; 
162-   } 
163184
164-   if  ( relativePath ?. startsWith ( '/closest-screenshot/' ) )  { 
165-     const  {  snapshotServer }  =  loadedTrace ( url ) ; 
166-     if  ( ! snapshotServer ) 
167-       return  new  Response ( null ,  {  status : 404  } ) ; 
168-     const  pageOrFrameId  =  relativePath . substring ( '/closest-screenshot/' . length ) ; 
169-     return  snapshotServer . serveClosestScreenshot ( pageOrFrameId ,  url . searchParams ) ; 
185+     // When trace viewer is deployed over https, we will force upgrade 
186+     // insecure http sub-resources to https. Otherwise, these will fail 
187+     // to load inside our https snapshots. 
188+     // In this case, we also match http resources from the archive by 
189+     // the https urls. 
190+     const  lookupUrls  =  [ request . url ] ; 
191+     if  ( isDeployedAsHttps  &&  request . url . startsWith ( 'https://' ) ) 
192+       lookupUrls . push ( request . url . replace ( / ^ h t t p s / ,  'http' ) ) ; 
193+     return  snapshotServer . serveResource ( lookupUrls ,  request . method ,  client ! . url ) ; 
170194  } 
171195
172-   if  ( relativePath ?. startsWith ( '/sha1/' ) )  { 
173-     const  {  traceModel }  =  loadedTrace ( url ) ; 
174-     const  blob  =  await  traceModel ?. resourceForSha1 ( relativePath . slice ( '/sha1/' . length ) ) ; 
175-     if  ( blob ) 
176-       return  new  Response ( blob ,  {  status : 200 ,  headers : downloadHeaders ( url . searchParams )  } ) ; 
177-     return  new  Response ( null ,  {  status : 404  } ) ; 
196+   // These commands all require a loaded trace. 
197+   if  ( relativePath  ===  '/contexts'  ||  relativePath ?. startsWith ( '/snapshotInfo/' )  ||  relativePath ?. startsWith ( '/closest-screenshot/' )  ||  relativePath ?. startsWith ( '/sha1/' ) )  { 
198+     if  ( ! client ) 
199+       return  new  Response ( 'Sub-resource without a client' ,  {  status : 500  } ) ; 
200+ 
201+     const  isContextRequest  =  relativePath  ===  '/contexts' ; 
202+     const  {  errorResponse,  loadedTrace }  =  await  loadTraceOrError ( client . id ,  url ,  isContextRequest ,  clientProgress ( client ) ) ; 
203+     if  ( errorResponse ) 
204+       return  errorResponse ; 
205+ 
206+     if  ( relativePath  ===  '/contexts' )  { 
207+       return  new  Response ( JSON . stringify ( loadedTrace ! . traceModel . contextEntries ) ,  { 
208+         status : 200 , 
209+         headers : {  'Content-Type' : 'application/json'  } 
210+       } ) ; 
211+     } 
212+ 
213+     if  ( relativePath ?. startsWith ( '/snapshotInfo/' ) )  { 
214+       const  pageOrFrameId  =  relativePath . substring ( '/snapshotInfo/' . length ) ; 
215+       return  loadedTrace ! . snapshotServer . serveSnapshotInfo ( pageOrFrameId ,  url . searchParams ) ; 
216+     } 
217+ 
218+     if  ( relativePath ?. startsWith ( '/closest-screenshot/' ) )  { 
219+       const  pageOrFrameId  =  relativePath . substring ( '/closest-screenshot/' . length ) ; 
220+       return  loadedTrace ! . snapshotServer . serveClosestScreenshot ( pageOrFrameId ,  url . searchParams ) ; 
221+     } 
222+ 
223+     if  ( relativePath ?. startsWith ( '/sha1/' ) )  { 
224+       const  blob  =  await  loadedTrace ! . traceModel . resourceForSha1 ( relativePath . slice ( '/sha1/' . length ) ) ; 
225+       if  ( blob ) 
226+         return  new  Response ( blob ,  {  status : 200 ,  headers : downloadHeaders ( url . searchParams )  } ) ; 
227+       return  new  Response ( null ,  {  status : 404  } ) ; 
228+     } 
178229  } 
179230
231+   // Pass through to the server for file requests. 
180232  if  ( relativePath ?. startsWith ( '/file/' ) )  { 
181233    const  path  =  url . searchParams . get ( 'path' ) ! ; 
182234    return  await  fetch ( traceFileURL ( path ) ) ; 
183235  } 
184236
185-   // Fallback for static assets. 
186-   if  ( relativePath ) 
187-     return  fetch ( event . request ) ; 
188- 
189-   const  snapshotUrl  =  client ! . url ; 
190-   const  traceUrl  =  new  URL ( snapshotUrl ) . searchParams . get ( 'trace' ) ! ; 
191-   const  {  snapshotServer }  =  loadedTraces . get ( traceUrl )  ||  { } ; 
192-   if  ( ! snapshotServer ) 
193-     return  new  Response ( null ,  {  status : 404  } ) ; 
194- 
195-   const  lookupUrls  =  [ request . url ] ; 
196-   if  ( isDeployedAsHttps  &&  request . url . startsWith ( 'https://' ) ) 
197-     lookupUrls . push ( request . url . replace ( / ^ h t t p s / ,  'http' ) ) ; 
198-   return  snapshotServer . serveResource ( lookupUrls ,  request . method ,  snapshotUrl ) ; 
237+   // Static content for sub-resource. 
238+   return  fetch ( event . request ) ; 
199239} 
200240
201241function  downloadHeaders ( searchParams : URLSearchParams ) : Headers  |  undefined  { 
@@ -210,13 +250,6 @@ function downloadHeaders(searchParams: URLSearchParams): Headers | undefined {
210250  return  headers ; 
211251} 
212252
213- const  emptyLoadedTrace  =  {  traceModel : undefined ,  snapshotServer : undefined  } ; 
214- 
215- function  loadedTrace ( url : URL ) : LoadedTrace  |  {  traceModel : undefined ,  snapshotServer : undefined  }  { 
216-   const  traceUrl  =  url . searchParams . get ( 'trace' ) ; 
217-   return  traceUrl  ? loadedTraces . get ( traceUrl )  ??  emptyLoadedTrace  : emptyLoadedTrace ; 
218- } 
219- 
220253async  function  gc ( )  { 
221254  const  clients  =  await  self . clients . matchAll ( ) ; 
222255  const  usedTraces  =  new  Set < string > ( ) ; 
@@ -236,7 +269,18 @@ async function gc() {
236269  } 
237270} 
238271
239- // @ts -ignore 
272+ function  clientProgress ( client : Client ) : Progress  { 
273+   return  ( done : number ,  total : number )  =>  { 
274+     client . postMessage ( {  method : 'progress' ,  params : {  done,  total }  } ) ; 
275+   } ; 
276+ } 
277+ 
278+ function  noopProgress ( done : number ,  total : number ) : undefined  {  } 
279+ 
280+ function  isLiveTrace ( traceUrl : string ) : boolean  { 
281+   return  traceUrl . endsWith ( '.json' ) ; 
282+ } 
283+ 
240284self . addEventListener ( 'fetch' ,  function ( event : FetchEvent )  { 
241285  event . respondWith ( doFetch ( event ) ) ; 
242286} ) ; 
0 commit comments