import { Awaitable , Context , Log , MiniflareError , ThrowingEventTarget , TypedEventListener , ValueOf , prefixError , } from "@miniflare/shared" ; import { Response as BaseResponse , fetch as baseFetch } from "undici" ; import { DOMException } from "./domexception" ; import { Request , Response , _kInner , withWaitUntil } from "./http" ; export type FetchErrorCode = "ERR_RESPONSE_TYPE" | "ERR_NO_UPSTREAM" | "ERR_NO_HANDLER" | "ERR_NO_RESPONSE" ; export class FetchError extends MiniflareError < FetchErrorCode > { } const SUGGEST_HANDLER = 'calling addEventListener("fetch", ...)' ; const SUGGEST_HANDLER_MODULES = "exporting a default object containing a `fetch` function property" ; const SUGGEST_RES = "calling `event.respondWith()` with a `Response` or `Promise<Response>` in your handler" ; const SUGGEST_RES_MODULES = "returning a `Response` in your handler" ; const SUGGEST_GLOBAL_BINDING_MODULES = "Attempted to access binding using global in modules." + "\nYou must use the 2nd `env` parameter passed to exported " + "handlers/Durable Object constructors, or `context.env` with " + "Pages Functions." ; const kResponse = Symbol ( "kResponse" ) ; const kPassThrough = Symbol ( "kPassThrough" ) ; const kWaitUntil = Symbol ( "kWaitUntil" ) ; const kSent = Symbol ( "kSent" ) ; export class FetchEvent extends Event { readonly request : Request ; [ kResponse ] ? : Promise < Response | BaseResponse > ; [ kPassThrough ] = false ; readonly [ kWaitUntil ] : Promise < unknown > [ ] = [ ] ; [ kSent ] = false ; constructor ( type , init ) { super ( type ) ; this . request = init . request ; } respondWith ( response ) { if ( ! ( this instanceof FetchEvent ) ) { throw new TypeError ( "Illegal invocation" ) ; } if ( this [ kResponse ] ) { throw new DOMException ( "FetchEvent.respondWith() has already been called; it can only be called once." , "InvalidStateError" ) ; } if ( this [ kSent ] ) { throw new DOMException ( "Too late to call FetchEvent.respondWith(). It must be called synchronously in the event handler." , "InvalidStateError" ) ; } this . stopImmediatePropagation ( ) ; this [ kResponse ] = Promise . resolve ( response ) ; } passThroughOnException ( ) { if ( ! ( this instanceof FetchEvent ) ) { throw new TypeError ( "Illegal invocation" ) ; } this [ kPassThrough ] = true ; } waitUntil ( promise ) { if ( ! ( this instanceof FetchEvent ) ) { throw new TypeError ( "Illegal invocation" ) ; } this [ kWaitUntil ] . push ( Promise . resolve ( promise ) ) ; } } export class ScheduledEvent extends Event { readonly scheduledTime : number ; readonly cron : string ; readonly [ kWaitUntil ] : Promise < unknown > [ ] = [ ] ; constructor ( type , init ) { super ( type ) ; this . scheduledTime = init . scheduledTime ; this . cron = init . cron ; } waitUntil ( promise ) { if ( ! ( this instanceof ScheduledEvent ) ) { throw new TypeError ( "Illegal invocation" ) ; } this [ kWaitUntil ] . push ( promise ) ; } } export class ExecutionContext { readonly #event : FetchEvent | ScheduledEvent ; constructor ( event ) { this . #event = event ; } passThroughOnException ( ) { if ( ! ( this instanceof ExecutionContext ) ) { throw new TypeError ( "Illegal invocation" ) ; } if ( this . #event instanceof FetchEvent ) this . #event . passThroughOnException ( ) ; } waitUntil ( promise ) { if ( ! ( this instanceof ExecutionContext ) ) { throw new TypeError ( "Illegal invocation" ) ; } this . #event . waitUntil ( promise ) ; } } export class ScheduledController { constructor ( public readonly scheduledTime , public readonly cron ) { } } export type ModuleFetchListener = ( request : Request , env : Context , ctx : ExecutionContext ) => Response | Promise < Response > ; export type ModuleScheduledListener = ( controller : ScheduledController , env : Context , ctx : ExecutionContext ) => any ; export const kAddModuleFetchListener = Symbol ( "kAddModuleFetchListener" ) ; export const kAddModuleScheduledListener = Symbol ( "kAddModuleScheduledListener" ) ; export const kDispatchFetch = Symbol ( "kDispatchFetch" ) ; export const kDispatchScheduled = Symbol ( "kDispatchScheduled" ) ; export const kDispose = Symbol ( "kDispose" ) ; export class PromiseRejectionEvent extends Event { readonly promise : Promise < any > ; readonly reason ? : any ; constructor ( type , init ) { super ( type , { cancelable : true } ) ; this . promise = init . promise ; this . reason = init . reason ; } } export type WorkerGlobalScopeEventMap = { fetch : FetchEvent ; scheduled : ScheduledEvent ; unhandledrejection : PromiseRejectionEvent ; rejectionhandled : PromiseRejectionEvent ; } ; export class WorkerGlobalScope extends ThrowingEventTarget < WorkerGlobalScopeEventMap > { } type PromiseListenerSetMember = TypedEventListener < PromiseRejectionEvent > | boolean ; type PromiseListener = { name : "unhandledRejection" ; set : Set < PromiseListenerSetMember > ; listener : ( reason : any , promise : Promise < any > ) => void ; } | { name : "rejectionHandled" ; set : Set < PromiseListenerSetMember > ; listener : ( promise : Promise < any > ) => void ; } ; export class ServiceWorkerGlobalScope extends WorkerGlobalScope { readonly #log : Log ; readonly #bindings : Context ; readonly #modules ? : boolean ; readonly #logUnhandledRejections ? : boolean ; #calledAddFetchEventListener = false ; readonly #unhandledRejection : PromiseListener ; readonly #rejectionHandled : PromiseListener ; readonly global = this ; readonly self = this ; constructor ( log , globals , bindings , modules ? , logUnhandledRejections ? ) { super ( ) ; this . #log = log ; this . #bindings = bindings ; this . #modules = modules ; this . #logUnhandledRejections = logUnhandledRejections ; this . #unhandledRejection = { name : "unhandledRejection" , set : new Set ( ) , listener : this . #unhandledRejectionListener , } ; this . #rejectionHandled = { name : "rejectionHandled" , set : new Set ( ) , listener : this . #rejectionHandledListener , } ; if ( this . #logUnhandledRejections ) { this . #maybeAddPromiseListener ( this . #unhandledRejection , true ) ; } Object . assign ( this , globals ) ; if ( modules ) { for ( const key of Object . keys ( bindings ) ) { if ( key === "__STATIC_CONTENT" || key === "__STATIC_CONTENT_MANIFEST" ) { break ; } Object . defineProperty ( this , key , { get ( ) { throw new ReferenceError ( `${ key } is not defined.\n${ SUGGEST_GLOBAL_BINDING_MODULES }` ) ; } , } ) ; } } else { Object . assign ( this , bindings ) ; } } addEventListener = < Type extends keyof WorkerGlobalScopeEventMap > ( type , listener , options ? ) : void => { if ( this . #modules ) { throw new TypeError ( "Global addEventListener() cannot be used in modules. Instead, event " + "handlers should be declared as exports on the root module." ) ; } if ( type === "fetch" ) this . #calledAddFetchEventListener = true ; if ( type === "unhandledrejection" && listener ) { this . #maybeAddPromiseListener ( this . #unhandledRejection , listener ) ; } if ( type === "rejectionhandled" && listener ) { this . #maybeAddPromiseListener ( this . #rejectionHandled , listener ) ; } super . addEventListener ( type , listener , options ) ; } ; removeEventListener = < Type extends keyof WorkerGlobalScopeEventMap > ( type , listener , options ? ) : void => { if ( this . #modules ) { throw new TypeError ( "Global removeEventListener() cannot be used in modules. Instead, event " + "handlers should be declared as exports on the root module." ) ; } if ( type === "unhandledrejection" && listener ) { this . #maybeRemovePromiseListener ( this . #unhandledRejection , listener ) ; } if ( type === "rejectionhandled" && listener ) { this . #maybeRemovePromiseListener ( this . #rejectionHandled , listener ) ; } super . removeEventListener ( type , listener , options ) ; } ; dispatchEvent = ( event ) : boolean => { if ( this . #modules ) { throw new TypeError ( "Global dispatchEvent() cannot be used in modules. Instead, event " + "handlers should be declared as exports on the root module." ) ; } return super . dispatchEvent ( event ) ; } ; [ kAddModuleFetchListener ] ( listener ) { this . #calledAddFetchEventListener = true ; super . addEventListener ( "fetch" , ( e ) => { const ctx = new ExecutionContext ( e ) ; const res = listener ( e . request , this . #bindings , ctx ) ; e . respondWith ( res ) ; } ) ; } [ kAddModuleScheduledListener ] ( listener ) { super . addEventListener ( "scheduled" , ( e ) => { const controller = new ScheduledController ( e . scheduledTime , e . cron ) ; const ctx = new ExecutionContext ( e ) ; const res = listener ( controller , this . #bindings , ctx ) ; if ( res !== undefined ) e . waitUntil ( Promise . resolve ( res ) ) ; } ) ; } async [ kDispatchFetch ] < WaitUntil extends any [ ] = unknown [ ] > ( request , proxy = false ) { const event = new FetchEvent ( "fetch" , { request : proxy ? request . clone ( ) : request , } ) ; let res ; try { super . dispatchEvent ( event ) ; res = await event [ kResponse ] ; } catch ( e ) { if ( event [ kPassThrough ] ) { this . #log . warn ( e . stack ) ; } else { throw e ; } } finally { event [ kSent ] = true ; } if ( res !== undefined ) { const validRes = res instanceof Response || res instanceof BaseResponse ; if ( ! validRes ) { const suggestion = this . #modules ? SUGGEST_RES_MODULES : SUGGEST_RES ; throw new FetchError ( "ERR_RESPONSE_TYPE" , `Fetch handler didn't respond with a Response object.\nMake sure you're ${ suggestion }.` ) ; } const waitUntil = Promise . all ( event [ kWaitUntil ] ) as Promise < WaitUntil > ; return withWaitUntil ( res , waitUntil ) ; } if ( ! proxy ) { if ( event [ kPassThrough ] ) { throw new FetchError ( "ERR_NO_UPSTREAM" , "No upstream to pass-through to specified.\nMake sure you've set the `upstream` option." ) ; } else if ( this . #calledAddFetchEventListener ) { const suggestion = this . #modules ? SUGGEST_RES_MODULES : SUGGEST_RES ; throw new FetchError ( "ERR_NO_RESPONSE" , `No fetch handler responded and no upstream to proxy to specified.\nMake sure you're ${ suggestion }.` ) ; } else { const suggestion = this . #modules ? SUGGEST_HANDLER_MODULES : SUGGEST_HANDLER ; throw new FetchError ( "ERR_NO_HANDLER" , `No fetch handler defined and no upstream to proxy to specified.\nMake sure you're ${ suggestion }.` ) ; } } const waitUntil = Promise . all ( event [ kWaitUntil ] ) as Promise < WaitUntil > ; return withWaitUntil ( await baseFetch ( request [ _kInner ] ) , waitUntil ) ; } async [ kDispatchScheduled ] < WaitUntil extends any [ ] = any [ ] > ( scheduledTime ? , cron ? ) { const event = new ScheduledEvent ( "scheduled" , { scheduledTime : scheduledTime ?? Date . now ( ) , cron : cron ?? "" , } ) ; super . dispatchEvent ( event ) ; return ( await Promise . all ( event [ kWaitUntil ] ) ) as WaitUntil ; } #maybeAddPromiseListener ( listener , member ) { if ( listener . set . size === 0 ) { this . #log . verbose ( `Adding process ${ listener . name } listener...` ) ; process . prependListener ( listener . name as any , listener . listener as any ) ; } listener . set . add ( member ) ; } #maybeRemovePromiseListener ( listener , member ) { const registered = listener . set . size > 0 ; listener . set . delete ( member ) ; if ( registered && listener . set . size === 0 ) { this . #log . verbose ( `Removing process ${ listener . name } listener...` ) ; process . removeListener ( listener . name , listener . listener ) ; } } #resetPromiseListener ( listener ) { if ( listener . set . size > 0 ) { this . #log . verbose ( `Removing process ${ listener . name } listener...` ) ; process . removeListener ( listener . name , listener . listener ) ; } listener . set . clear ( ) ; } #unhandledRejectionListener = ( reason , promise ) : void => { const event = new PromiseRejectionEvent ( "unhandledrejection" , { reason , promise , } ) ; const notCancelled = super . dispatchEvent ( event ) ; if ( notCancelled ) { if ( this . #logUnhandledRejections ) { this . #log . error ( prefixError ( "Unhandled Promise Rejection" , reason ) ) ; } else { this . #resetPromiseListener ( this . #unhandledRejection ) ; Promise . reject ( reason ) ; } } } ; #rejectionHandledListener = ( promise ) : void => { const event = new PromiseRejectionEvent ( "rejectionhandled" , { promise } ) ; super . dispatchEvent ( event ) ; } ; [ kDispose ] ( ) { this . #resetPromiseListener ( this . #unhandledRejection ) ; this . #resetPromiseListener ( this . #rejectionHandled ) ; } }