import Protocol from 'devtools-protocol' ; import { RequestMapping , ResponseMapping , MappedEvent , EventMapping , EventPredicate , MappedRequestMethod } from 'chrome-debugging-client' ; import { RaceCancellation } from 'race-cancellation' ; export interface VersionStatusIdentifier { status : Protocol . ServiceWorker . ServiceWorkerVersionStatus ; version ? : string ; runningStatus ? : Protocol . ServiceWorker . ServiceWorkerVersionRunningStatus ; } type SerializedStateIdentifier = string ; function identifierFromVersion ( v ) { return { version : v . versionId , status : v . status , runningStatus : v . runningStatus } ; } function serializeStateIdentifier ( { version , status , runningStatus } ) { const sanitizedVersion = version ? version : '' ; const sanitizedRunningStatus = runningStatus ? runningStatus : '' ; const id = `${ sanitizedVersion }:${ status }:${ sanitizedRunningStatus }` ; return id ; } function addTimeout < T > ( promise , msg , timeout = 6000 ) { const timedOut = new Promise ( ( _resolve , reject ) => { setTimeout ( ( ) => { reject ( new Error ( msg ) ) ; } , timeout ) ; } ) ; return Promise . race ( [ promise , timedOut ] ) ; } class StateIdMap < V > extends Map < any , V > { get ( key ) { return super . get ( serializeStateIdentifier ( key ) ) ; } set ( key , value ) { return super . set ( serializeStateIdentifier ( key ) , value ) ; } } class StateIdArrayMap < V > extends Map < any , V [ ] > { get ( key ) { const versionQuery = super . get ( serializeStateIdentifier ( key ) ) ; const eventQuery = super . get ( serializeStateIdentifier ( { status : key . status , runningStatus : key . runningStatus } ) ) ; if ( ! versionQuery && ! eventQuery ) { return ; } return ( versionQuery || [ ] ) . concat ( eventQuery || [ ] ) ; } set ( key , value ) { return super . set ( serializeStateIdentifier ( key ) , value ) ; } } public type VersionListener = ( v : Protocol . ServiceWorker . ServiceWorkerVersion ) => void ; internal export interface IServiceWorkerStateOptions { log ? : boolean ; } export type ServiceWorkerErrorCallback = ( err : Protocol . ServiceWorker . ServiceWorkerErrorMessage ) => void ; export interface IPage { send < M extends MappedRequestMethod > ( method , request , raceCancellation ? ) : Promise < ResponseMapping [ M ] > ; on < E extends MappedEvent > ( event , listener ) : void ; until < E extends MappedEvent > ( event , predicate ? , raceCancellation ? ) : Promise < EventMapping [ E ] > ; } remarks Effectively a wrapper around the  https * | ServiceWorker} domain of the Chrome DevTools Protocol.
 *  internal export class ServiceWorkerState { private versions : Map < number , Protocol . ServiceWorker . ServiceWorkerVersion > ; private active : Protocol . ServiceWorker . ServiceWorkerVersion ; private lastInstalled : Protocol . ServiceWorker . ServiceWorkerVersion ; private log : boolean ; private errors : Protocol . ServiceWorker . ServiceWorkerErrorMessage [ ] ; private stateListeners : StateIdArrayMap < VersionListener > ; private stateHistory : StateIdMap < Protocol . ServiceWorker . ServiceWorkerVersion > ; private page : IPage ; private errorCallbacks : ServiceWorkerErrorCallback [ ] ; constructor ( page , options = { } ) { this . versions = new Map ( ) ; this . stateListeners = new StateIdArrayMap ( ) ; this . stateHistory = new StateIdMap ( ) ; this . log = ! ! options . log ; this . errorCallbacks = [ ] ; this . errors = [ ] ; this . page = page ; page . on ( 'ServiceWorker.workerVersionUpdated' , ( { versions } ) => { for ( let version of versions ) { this . recordVersion ( version ) ; } } ) ; page . on ( 'ServiceWorker.workerErrorReported' , ( { errorMessage } ) => { this . errors . push ( errorMessage ) ; } ) ; } public catchErrors ( cb ) { this . errorCallbacks . push ( cb ) ; } public getErrors ( ) { return this . errors ; } public ensureNoErrors ( handleError ? ) { const errors = this . errors ; this . errors = [ ] ; const cbs = this . errorCallbacks . length ; if ( errors . length ) { if ( cbs > 0 ) { for ( let i = 0 ; i < errors . length ; i ++ ) { for ( let x = 0 ; x < cbs ; x ++ ) { this . errorCallbacks [ x ] ( errors [ i ] ) ; } } } else { const err = new Error ( `Uncaught error thrown in Service Worker: ${ errors [ 0 ] . errorMessage }` ) ; if ( handleError ) { handleError ( err ) ; } else { throw err ; } } } } private listen ( id , listener ) { let listeners = this . stateListeners . get ( id ) || [ ] ; listeners . push ( listener ) ; this . stateListeners . set ( id , listeners ) ; } private recordVersion ( version ) { if ( this . log ) { console . log ( '[sw]' , version . status , version . runningStatus , version . versionId ) ; } this . versions . set ( Number ( version . versionId ) , version ) ; const id = identifierFromVersion ( version ) ; this . stateHistory . set ( id , version ) ; if ( version . status === 'activated' && version . runningStatus === 'running' ) { this . handleActivated ( version ) ; } else if ( version . status === 'installed' && version . runningStatus === 'running' ) { this . handleInstalled ( version ) ; } const listeners = this . stateListeners . get ( id ) ; if ( listeners ) { listeners . forEach ( ( listener ) => { listener ( version ) ; } ) ; } } public debug ( ) { this . log = true ; } public waitForInstalled ( version ? ) { if ( ! version && this . lastInstalled ) { return Promise . resolve ( this . lastInstalled ) ; } return this . waitForState ( { status : 'installed' , version } ) ; } public waitForActivated ( version ? ) { if ( ! version && this . active ) { return Promise . resolve ( this . active ) ; } return this . waitForState ( { status : 'activated' , version } ) ; } public waitForActivation ( ) { return this . waitForState ( 'activated' ) ; } public waitForInstallation ( ) { return this . waitForState ( 'installed' ) ; } public waitForNextInstalled ( version ) { return this . page . until ( 'ServiceWorker.workerVersionUpdated' , ( { versions } ) => { for ( let i = 0 ; i < versions . length ; i ++ ) { const { versionId , status , runningStatus } = versions [ i ] ; if ( status === 'installed' && runningStatus === 'running' && versionId > version . versionId ) { return true ; } } return false ; } ) ; } public waitForVersionUpdate ( predicate , label , timeout = 1000 ) { return Promise . race ( [ this . page . until ( 'ServiceWorker.workerVersionUpdated' , ( { versions } ) => { for ( let i = 0 ; i < versions . length ; i ++ ) { if ( predicate ( versions [ i ] ) ) { return true ; } } return false ; } ) , new Promise ( ( _r , reject ) => { setTimeout ( ( ) => { reject ( `Timeout waiting for service worker version update: ${ label }` ) ; } , timeout ) ; } ) ] ) as Promise < Protocol . ServiceWorker . WorkerVersionUpdatedEvent > ; } public waitForState ( arg ) { const id = typeof arg === 'string' ? { status : arg } : arg ; if ( ! id . runningStatus ) { id . runningStatus = 'running' ; } const existingHistory = this . stateHistory . get ( id ) ; if ( existingHistory ) { return Promise . resolve ( existingHistory ) ; } return addTimeout ( new Promise ( ( resolve ) => { this . listen ( id , ( result ) => { Promise . resolve ( ) . then ( ( ) => { resolve ( result ) ; } ) ; } ) ; } ) , `Waiting for service worker version ${ id . version } to be ${ id . status } timed out` , 10000 ) ; } private handleActivated ( version ) { this . active = version ; } private handleInstalled ( version ) { this . lastInstalled = version ; } public getActive ( ) { if ( ! this . active ) { throw new Error ( 'Error calling getActive(): There is no active worker yet. Try using waitForActivated()' ) ; } return this . active ; } public getLastInstalled ( ) { return this . lastInstalled ; } public skipWaiting ( ) { return this . page . send ( 'ServiceWorker.skipWaiting' , { scopeURL : '/' } ) ; } public emulateOffline ( offline ) { throw new Error ( 'Offline emulation not working. See https://bugs.chromium.org/p/chromium/issues/detail?id=852127' ) ; } public close ( ) { this . versions . clear ( ) ; this . stateListeners . clear ( ) ; this . stateHistory . clear ( ) ; this . errorCallbacks = [ ] ; } }