import fs from "fs/promises" ; import path from "path" ; import { URL } from "url" ; import { AdditionalModules , BeforeSetupResult , Compatibility , Context , Log , Mutex , Options , PluginContext , PluginEntries , PluginOptions , PluginOptionsUnion , PluginSignatures , RequestContext , ScriptBlueprint , ScriptRunner , ScriptRunnerResult , SetupResult , StorageFactory , TypedEventTarget , WranglerConfig , addAll , logOptions , resolveStoragePersist , } from "@miniflare/shared" ; import type { Watcher } from "@miniflare/watcher" ; import { dequal } from "dequal/lite" ; import { dim } from "kleur/colors" ; import { MiniflareCoreError } from "./error" ; import { formatSize , pathsToString } from "./helpers" ; import { BindingsPlugin , CorePlugin , _CoreMount , _populateBuildConfig , } from "./plugins" ; import { Router } from "./router" ; import { Request , RequestInfo , RequestInit , Response , ServiceWorkerGlobalScope , _kLoopHeader , kAddModuleFetchListener , kAddModuleScheduledListener , kDispatchFetch , kDispatchScheduled , kDispose , withImmutableHeaders , withStringFormDataFiles , } from "./standards" ; import { PluginStorageFactory } from "./storage" ; export * from "./plugins" ; export * from "./standards" ; export * from "./error" ; export * from "./router" ; export * from "./storage" ; internal export function _deepEqual ( a , b ) { if ( ! dequal ( a , b ) ) return false ; if ( typeof a === "object" ) { const aSymbols = Object . getOwnPropertySymbols ( a ) ; for ( const aSymbol of aSymbols ) { if ( ! ( aSymbol in b ) || ! dequal ( a [ aSymbol ] , b [ aSymbol ] ) ) return false ; } return aSymbols . length === Object . getOwnPropertySymbols ( b ) . length ; } return true ; } export type CorePluginSignatures = PluginSignatures & { CorePlugin : typeof CorePlugin ; } ; export type PluginInstances < Plugins extends PluginSignatures > = { [ K in keyof Plugins ] : InstanceType < Plugins [ K ] > ; } ; type PluginData < Plugins extends PluginSignatures , Data > = Map < keyof Plugins , Data > ; export type MiniflareCoreOptions < Plugins extends CorePluginSignatures > = Omit < Options < Plugins > , "mounts" > & { mounts ? : Record < string , string | Omit < Options < Plugins > , "mounts" > > ; } ; function getPluginEntries < Plugins extends PluginSignatures > ( plugins ) { const entries = Object . entries ( plugins ) as PluginEntries < Plugins > ; let coreIndex = - 1 ; let bindingsIndex = - 1 ; for ( let i = 0 ; i < entries . length ; i ++ ) { const [ , plugin ] = entries [ i ] ; if ( plugin === CorePlugin ) coreIndex = i ; else if ( plugin === BindingsPlugin ) bindingsIndex = i ; } if ( coreIndex > 0 ) { entries . unshift ( ... entries . splice ( coreIndex , 1 ) ) ; } if ( bindingsIndex !== - 1 && bindingsIndex !== entries . length - 1 ) { entries . push ( ... entries . splice ( bindingsIndex , 1 ) ) ; } return entries ; } function splitPluginOptions < Plugins extends CorePluginSignatures > ( plugins , options ) { const result = { } as PluginOptions < Plugins > ; for ( const [ name , plugin ] of plugins ) { const pluginResult = { } as PluginOptionsUnion < Plugins > ; for ( const key of plugin . prototype . opts ?. keys ( ) ?? [ ] ) { if ( key in ( options as any ) ) { ( pluginResult as any ) [ key ] = ( options as any ) [ key ] ; } } result [ name ] = pluginResult ; } return result ; } function splitWranglerConfig < Plugins extends PluginSignatures > ( plugins , overrides , config , configDir ) { const result = { } as PluginOptions < Plugins > ; for ( const [ name , plugin ] of plugins ) { const pluginResult = { } as PluginOptionsUnion < Plugins > ; const pluginOverrides = overrides [ name ] ; for ( const [ key , meta ] of plugin . prototype . opts ?. entries ( ) ?? [ ] ) { if ( key in pluginOverrides ) { ( pluginResult as any ) [ key ] = pluginOverrides [ key ] ; } else { ( pluginResult as any ) [ key ] = meta . fromWrangler ?. ( config , configDir ) ; } } result [ name ] = pluginResult ; } return result ; } const pathResolve = ( p ) => path . resolve ( p ) ; function throwNoScriptError ( modules ? ) { const execName = process . env . MINIFLARE_EXEC_NAME ?? "miniflare" ; const script = modules ? "worker.mjs" : "worker.js" ; const format = modules ? "modules" : "service-worker" ; const pkgScriptField = modules ? "module" : "main" ; const lines = [ "No script defined, either:" , "- Pass it as a positional argument, if you're using the CLI" , dim ( `    $ ${ execName } dist/${ script }` ) , "- Set the script or scriptPath option, if you're using the API" , dim ( `    new Miniflare({ scriptPath: "dist/${ script }" })` ) , `- Set ${ pkgScriptField } in package.json` , dim ( `    { "${ pkgScriptField }": "dist/${ script }" }` ) , ] ; if ( modules ) { lines . push ( "- Set build.upload.main in wrangler.toml" , dim ( "    [build.upload]" ) , dim ( `    format = "${ format }"` ) , dim ( `    dir = "dist"` ) , dim ( `    main = "${ script }"` ) ) ; } lines . push ( "" ) ; throw new MiniflareCoreError ( "ERR_NO_SCRIPT" , lines . join ( "\n" ) ) ; } export interface MiniflareCoreContext { log : Log ; storageFactory : StorageFactory ; scriptRunner ? : ScriptRunner ; scriptRequired ? : boolean ; scriptRunForModuleExports ? : boolean ; isMount ? : boolean ; } export class ReloadEvent < Plugins extends PluginSignatures > extends Event { readonly plugins : PluginInstances < Plugins > ; readonly initial : boolean ; constructor ( type , init ) { super ( type ) ; this . plugins = init . plugins ; this . initial = init . initial ; } } export type MiniflareCoreEventMap < Plugins extends PluginSignatures > = { reload : ReloadEvent < Plugins > ; } ; export class MiniflareCore < Plugins extends CorePluginSignatures > extends TypedEventTarget < MiniflareCoreEventMap < Plugins > > { readonly #originalPlugins : Plugins ; readonly #plugins : PluginEntries < Plugins > ; #previousSetOptions : MiniflareCoreOptions < Plugins > ; #overrides : PluginOptions < Plugins > ; #previousOptions ? : PluginOptions < Plugins > ; readonly #ctx : MiniflareCoreContext ; readonly #pluginStorages : PluginData < Plugins , PluginStorageFactory > ; #compat ? : Compatibility ; #previousRootPath ? : string ; #previousGlobalAsyncIO ? : boolean ; #instances ? : PluginInstances < Plugins > ; #mounts ? : Map < string , MiniflareCore < Plugins > > ; #router ? : Router ; #wranglerConfigPath ? : string ; #watching ? : boolean ; #beforeSetupWatch ? : PluginData < Plugins , Set < string > > ; #setupWatch ? : PluginData < Plugins , Set < string > > ; #setupResults ? : PluginData < Plugins , SetupResult > ; readonly #scriptWatchPaths = new Set < string > ( ) ; #reloaded = false ; #globalScope ? : ServiceWorkerGlobalScope ; #bindings ? : Context ; #moduleExports ? : Context ; #watcher ? : Watcher ; #watcherCallbackMutex ? : Mutex ; #previousWatchPaths ? : Set < string > ; constructor ( plugins , ctx , options = { } as MiniflareCoreOptions < Plugins > ) { super ( ) ; this . #originalPlugins = plugins ; this . #plugins = getPluginEntries ( plugins ) ; this . #previousSetOptions = options ; this . #overrides = splitPluginOptions ( this . #plugins , options ) ; this . #ctx = ctx ; this . #pluginStorages = new Map < keyof Plugins , PluginStorageFactory > ( ) ; this . #initPromise = this . #init ( ) . then ( ( ) => this . #reload ( ) ) ; } #updateWatch ( data , name , result ) { if ( this . #watching && result ?. watch ) { const resolved = result . watch . map ( pathResolve ) ; data . set ( name , new Set ( resolved ) ) ; } else { data . delete ( name ) ; } } async #runBeforeSetup ( name ) { const instance = this . #instances ! [ name ] ; if ( ! instance . beforeSetup ) return false ; this . #ctx . log . verbose ( `- beforeSetup(${ name })` ) ; const result = await instance . beforeSetup ( ) ; this . #updateWatch ( this . #beforeSetupWatch ! , name , result ) ; return true ; } async #runSetup ( name ) { const instance = this . #instances ! [ name ] ; if ( ! instance . setup ) return false ; this . #ctx . log . verbose ( `- setup(${ name })` ) ; const result = await instance . setup ( this . getPluginStorage ( name ) ) ; this . #updateWatch ( this . #setupWatch ! , name , result ) ; this . #setupResults ! . set ( name , result ?? { } ) ; return true ; } readonly #initPromise : Promise < void > ; async #init ( reloadAll = false ) { this . #ctx . log . debug ( "Initialising worker..." ) ; const previous = this . #previousOptions ; let options = this . #overrides ; const rootPath = options . CorePlugin . rootPath ?? process . cwd ( ) ; const originalConfigPath = options . CorePlugin . wranglerConfigPath ; const configEnv = options . CorePlugin . wranglerConfigEnv ; let configPath = originalConfigPath === true ? "wrangler.toml" : originalConfigPath ; if ( configPath ) { configPath = path . resolve ( rootPath , configPath ) ; this . #wranglerConfigPath = configPath ; try { const configData = await fs . readFile ( configPath , "utf8" ) ; const toml = require ( "@iarna/toml" ) ; const config = toml . parse ( configData ) ; if ( configEnv && config . env && configEnv in config . env ) { Object . assign ( config , config . env [ configEnv ] ) ; } const configDir = path . dirname ( configPath ) ; _populateBuildConfig ( config , configDir , configEnv ) ; options = splitWranglerConfig ( this . #plugins , this . #overrides , config , configDir ) ; } catch ( e ) { if ( ! ( e . code === "ENOENT" && originalConfigPath === true ) ) { throw e ; } } } this . #watching ??= options . CorePlugin . watch ?? false ; const { compatibilityDate , compatibilityFlags , globalAsyncIO } = options . CorePlugin ; let ctxUpdate = ( this . #previousRootPath && this . #previousRootPath !== rootPath ) || this . #previousGlobalAsyncIO !== globalAsyncIO || reloadAll ; this . #previousRootPath = rootPath ; if ( this . #compat ) { if ( this . #compat . update ( compatibilityDate , compatibilityFlags ) ) { ctxUpdate = true ; } } else { this . #compat = new Compatibility ( compatibilityDate , compatibilityFlags ) ; } const ctx = { log : this . #ctx . log , compat : this . #compat , rootPath , globalAsyncIO , } ; logOptions ( this . #plugins , this . #ctx . log , options ) ; const enabled = this . #compat . enabled ; this . #ctx . log . debug ( `Enabled Compatibility Flags:${ enabled . length === 0 ? " <none>" : "" }` ) ; for ( const flag of enabled ) this . #ctx . log . debug ( `- ${ flag }` ) ; this . #instances ??= { } as PluginInstances < Plugins > ; this . #beforeSetupWatch ??= new Map < keyof Plugins , Set < string > > ( ) ; let ranBeforeSetup = false ; for ( const [ name , plugin ] of this . #plugins ) { if ( previous !== undefined && ! ctxUpdate && _deepEqual ( previous [ name ] , options [ name ] ) ) { continue ; } const existingInstance = this . #instances [ name ] ; if ( existingInstance ?. dispose ) { this . #ctx . log . verbose ( `- dispose(${ name })` ) ; await existingInstance . dispose ( ) ; } const instance = new plugin ( ctx , options [ name ] ) ; this . #instances [ name ] = instance as any ; if ( await this . #runBeforeSetup ( name ) ) ranBeforeSetup = true ; } this . #setupWatch ??= new Map < keyof Plugins , Set < string > > ( ) ; this . #setupResults ??= new Map < keyof Plugins , SetupResult > ( ) ; for ( const [ name ] of this . #plugins ) { if ( previous !== undefined && ! ctxUpdate && _deepEqual ( previous [ name ] , options [ name ] ) && ! ( ranBeforeSetup && this . #setupResults . get ( name ) ?. script ) ) { continue ; } await this . #runSetup ( name ) ; } this . #previousOptions = options ; this . #mounts ??= new Map ( ) ; const mounts = options . CorePlugin . mounts as MiniflareCoreOptions < Plugins > [ "mounts" ] ; if ( mounts ) { const defaultMountOptions = { watch : this . #watching || undefined } ; const kvPersist = resolveStoragePersist ( rootPath , options . KVPlugin ?. kvPersist ) ; const cachePersist = resolveStoragePersist ( rootPath , options . CachePlugin ?. cachePersist ) ; const durableObjectsPersist = resolveStoragePersist ( rootPath , options . DurableObjectsPlugin ?. durableObjectsPersist ) ; if ( kvPersist !== undefined ) { defaultMountOptions . kvPersist = kvPersist ; } if ( cachePersist !== undefined ) { defaultMountOptions . cachePersist = cachePersist ; } if ( durableObjectsPersist !== undefined ) { defaultMountOptions . durableObjectsPersist = durableObjectsPersist ; } for ( const [ name , rawOptions ] of Object . entries ( mounts ) ) { if ( name === "" ) { throw new MiniflareCoreError ( "ERR_MOUNT_NO_NAME" , "Mount name cannot be empty" ) ; } const mountOptions = typeof rawOptions === "string" ? ( { ... defaultMountOptions , rootPath : rawOptions , packagePath : true , envPath : true , wranglerConfigPath : true , } as any ) : { ... defaultMountOptions , ... rawOptions , } ; if ( "mounts" in mountOptions ) { throw new MiniflareCoreError ( "ERR_MOUNT_NESTED" , "Nested mounts are unsupported" ) ; } let mount = this . #mounts . get ( name ) ; if ( mount ) { this . #ctx . log . verbose ( `Updating mount \"${ name }\"...` ) ; await mount . setOptions ( mountOptions , false ) ; } else { this . #ctx . log . debug ( `Mounting \"${ name }\"...` ) ; let log = this . #ctx . log ; if ( Object . getPrototypeOf ( this . #ctx . log ) === Log . prototype ) { log = new Log ( this . #ctx . log . level , { suffix : name } ) ; } const ctx = { ... this . #ctx , log , scriptRunForModuleExports : false , isMount : true , } ; mount = new MiniflareCore ( this . #originalPlugins , ctx , mountOptions ) ; mount . addEventListener ( "reload" , async ( event ) => { if ( ! event . initial ) { try { await this . #updateRouter ( ) ; await this . #reload ( ) ; } catch ( e ) { this . #ctx . log . error ( e ) ; } } } ) ; try { await mount . getPlugins ( ) ; } catch ( e ) { throw new MiniflareCoreError ( "ERR_MOUNT" , `Error mounting \"${ name }\"` , e ) ; } this . #mounts . set ( name , mount ) ; } } } for ( const [ name , mount ] of [ ... this . #mounts ] ) { if ( mounts === undefined || ! ( name in mounts ) ) { this . #ctx . log . debug ( `Unmounting \"${ name }\"...` ) ; await mount . dispose ( ) ; this . #mounts . delete ( name ) ; } } await this . #updateRouter ( ) ; if ( this . #ctx . scriptRequired && ! this . #setupResults . get ( "CorePlugin" ) ?. script && this . #mounts . size === 0 ) { throwNoScriptError ( options . CorePlugin . modules ) ; } } async #updateRouter ( ) { const allRoutes = new Map < string , string [ ] > ( ) ; const { CorePlugin } = this . #instances ! ; if ( CorePlugin . name ) { const routes = CorePlugin . routes ; if ( routes ) allRoutes . set ( CorePlugin . name , routes ) ; } for ( const [ name , mount ] of this . #mounts ! ) { const { CorePlugin } = await mount . getPlugins ( ) ; if ( CorePlugin . name !== undefined && CorePlugin . name !== name ) { throw new MiniflareCoreError ( "ERR_MOUNT_NAME_MISMATCH" , `Mounted name "${ name }" must match service name "${ CorePlugin . name }"` ) ; } const routes = CorePlugin . routes ; if ( routes ) allRoutes . set ( name , routes ) ; } this . #router ??= new Router ( ) ; this . #router . update ( allRoutes ) ; if ( this . #mounts ! . size ) { this . #ctx . log . debug ( `Mount Routes:${ this . #router . routes . length === 0 ? " <none>" : "" }` ) ; for ( let i = 0 ; i < this . #router . routes . length ; i ++ ) { const route = this . #router . routes [ i ] ; this . #ctx . log . debug ( `${ i + 1 }. ${ route . route } => ${ route . target }` ) ; } } } async #runAllBeforeReloads ( ) { for ( const [ name ] of this . #plugins ) { const instance = this . #instances ! [ name ] ; if ( instance . beforeReload ) { this . #ctx . log . verbose ( `- beforeReload(${ name })` ) ; await instance . beforeReload ( ) ; } } } async #runAllReloads ( mounts ) { const bindings = this . #bindings ; const exports = this . #moduleExports ; for ( const [ name ] of this . #plugins ) { const instance = this . #instances ! [ name ] ; if ( instance . reload ) { this . #ctx . log . verbose ( `- reload(${ name })` ) ; await instance . reload ( bindings ?? { } , exports ?? { } , mounts ) ; } } } async #reload ( dispatchReloadEvent = true ) { this . #ctx . log . debug ( "Reloading worker..." ) ; const globals = { } ; const bindings = { } ; const newWatchPaths = new Set < string > ( ) ; if ( this . #wranglerConfigPath ) newWatchPaths . add ( this . #wranglerConfigPath ) ; await this . #runAllBeforeReloads ( ) ; if ( ! this . #ctx . isMount ) { for ( const mount of this . #mounts ! . values ( ) ) { await mount . #runAllBeforeReloads ( ) ; } } let script = undefined ; let requiresModuleExports = false ; const additionalModules = { } ; for ( const [ name ] of this . #plugins ) { const result = this . #setupResults ! . get ( name ) ; Object . assign ( globals , result ?. globals ) ; Object . assign ( bindings , result ?. bindings ) ; if ( result ?. script ) { if ( script ) { throw new TypeError ( "Multiple plugins returned a script" ) ; } script = result . script ; } if ( result ?. requiresModuleExports ) requiresModuleExports = true ; if ( result ?. additionalModules ) { Object . assign ( additionalModules , result . additionalModules ) ; } const beforeSetupWatch = this . #beforeSetupWatch ! . get ( name ) ; if ( beforeSetupWatch ) addAll ( newWatchPaths , beforeSetupWatch ) ; const setupWatch = this . #setupWatch ! . get ( name ) ; if ( setupWatch ) addAll ( newWatchPaths , setupWatch ) ; } const { modules , processedModuleRules , logUnhandledRejections } = this . #instances ! . CorePlugin ; this . #globalScope ?. [ kDispose ] ( ) ; const globalScope = new ServiceWorkerGlobalScope ( this . #ctx . log , globals , bindings , modules , logUnhandledRejections ) ; this . #globalScope = globalScope ; this . #bindings = bindings ; this . #moduleExports = { } ; const rules = modules ? processedModuleRules : undefined ; let res = undefined ; if ( script && ( ! this . #ctx . scriptRunForModuleExports || ( modules && requiresModuleExports ) ) ) { if ( ! this . #ctx . scriptRunner ) { throw new TypeError ( "Running scripts requires a script runner" ) ; } this . #ctx . log . verbose ( "Running script..." ) ; res = await this . #ctx . scriptRunner . run ( globalScope , script , rules , additionalModules ) ; this . #scriptWatchPaths . clear ( ) ; this . #scriptWatchPaths . add ( script . filePath ) ; if ( res . watch ) { addAll ( newWatchPaths , res . watch ) ; addAll ( this . #scriptWatchPaths , res . watch ) ; } this . #moduleExports = res . exports ; if ( res . exports ) { const defaults = res . exports . default ; const fetchListener = defaults ?. fetch ?. bind ( defaults ) ; if ( fetchListener ) { globalScope [ kAddModuleFetchListener ] ( fetchListener ) ; } const scheduledListener = defaults ?. scheduled ?. bind ( defaults ) ; if ( scheduledListener ) { globalScope [ kAddModuleScheduledListener ] ( scheduledListener ) ; } } } if ( ! this . #ctx . isMount ) { const mounts = new Map < string , _CoreMount > ( ) ; const name = this . #instances ! . CorePlugin . name ; if ( name ) { mounts . set ( name , { moduleExports : this . #moduleExports , dispatchFetch : ( request ) => this [ kDispatchFetch ] ( request , true ) , } as _CoreMount ) ; } for ( const [ name , mount ] of this . #mounts ! ) { mounts . set ( name , { moduleExports : await mount . getModuleExports ( ) , dispatchFetch : ( request ) => mount [ kDispatchFetch ] ( request , true ) , } as _CoreMount ) ; } await this . #runAllReloads ( mounts ) ; for ( const mount of this . #mounts ! . values ( ) ) { await mount . #runAllReloads ( mounts ) ; } } if ( dispatchReloadEvent ) { const reloadEvent = new ReloadEvent ( "reload" , { plugins : this . #instances ! , initial : ! this . #reloaded , } ) ; this . dispatchEvent ( reloadEvent ) ; } this . #reloaded = true ; this . #ctx . log . info ( `Worker reloaded!${ res ?. bundleSize !== undefined ? ` (${ formatSize ( res . bundleSize ) })` : "" }` ) ; if ( res ?. bundleSize !== undefined && res . bundleSize > 1048576 ) { this . #ctx . log . warn ( "Worker's uncompressed size exceeds the 1MiB limit! " + "Note that your worker will be compressed during upload " + "so you may still be able to deploy it." ) ; } if ( this . #watching ) { let watcher = this . #watcher ; if ( ! watcher ) { const { Watcher , } = require ( "@miniflare/watcher" ) ; this . #watcherCallbackMutex = new Mutex ( ) ; watcher = new Watcher ( this . #watcherCallback . bind ( this ) ) ; this . #watcher = watcher ; } const unwatchedPaths = new Set < string > ( ) ; const watchedPaths = new Set < string > ( ) ; for ( const watchedPath of this . #previousWatchPaths ?? [ ] ) { if ( ! newWatchPaths . has ( watchedPath ) ) { unwatchedPaths . add ( watchedPath ) ; } } for ( const newWatchedPath of newWatchPaths ) { if ( ! this . #previousWatchPaths ?. has ( newWatchedPath ) ) { watchedPaths . add ( newWatchedPath ) ; } } if ( unwatchedPaths . size > 0 ) { this . #ctx . log . debug ( `Unwatching ${ pathsToString ( unwatchedPaths ) }...` ) ; watcher . unwatch ( unwatchedPaths ) ; } if ( watchedPaths . size > 0 ) { this . #ctx . log . debug ( `Watching ${ pathsToString ( newWatchPaths ) }...` ) ; watcher . watch ( watchedPaths ) ; } this . #previousWatchPaths = newWatchPaths ; } } #ignoreScriptUpdates = false ; #ignoreScriptUpdatesTimeout ! : NodeJS . Timeout ; #watcherCallback ( eventPath ) { this . #ctx . log . debug ( `${ path . relative ( "" , eventPath ) } changed...` ) ; if ( this . #ignoreScriptUpdates && this . #scriptWatchPaths . has ( eventPath ) ) { this . #ctx . log . verbose ( "Ignoring script change after build..." ) ; return ; } const promise = this . #watcherCallbackMutex ! . runWith ( async ( ) => { if ( eventPath === this . #wranglerConfigPath ) { await this . #init ( ) ; } let ranBeforeSetup = false ; for ( const [ name ] of this . #plugins ) { if ( this . #beforeSetupWatch ! . get ( name ) ?. has ( eventPath ) ) { await this . #runBeforeSetup ( name ) ; ranBeforeSetup = true ; this . #ignoreScriptUpdates = true ; clearTimeout ( this . #ignoreScriptUpdatesTimeout ) ; this . #ignoreScriptUpdatesTimeout = setTimeout ( ( ) => ( this . #ignoreScriptUpdates = false ) , 1000 ) ; } if ( this . #setupWatch ! . get ( name ) ?. has ( eventPath ) ) { await this . #runSetup ( name ) ; } } if ( ranBeforeSetup ) { for ( const [ name ] of this . #plugins ) { if ( this . #setupResults ! . get ( name ) ?. script ) { await this . #runSetup ( name ) ; } } } if ( ! this . #watcherCallbackMutex ! . hasWaiting ) { await this . #reload ( ) ; } } ) ; promise . catch ( ( e ) => this . #ctx . log . error ( e ) ) ; } get log ( ) : Log { return this . #ctx . log ; } async reload ( ) { await this . #initPromise ; await this . #init ( true ) ; await this . #reload ( ) ; } async setOptions ( options , dispatchReloadEvent = true ) { await this . #initPromise ; options = { ... this . #previousSetOptions , ... options } ; this . #previousSetOptions = options ; this . #overrides = splitPluginOptions ( this . #plugins , options ) ; await this . #init ( ) ; await this . #reload ( dispatchReloadEvent ) ; } getPluginStorage ( name ) { let storage = this . #pluginStorages . get ( name ) ; if ( storage ) return storage ; this . #pluginStorages . set ( name , ( storage = new PluginStorageFactory ( this . #ctx . storageFactory , name as string ) ) ) ; return storage ; } async getPlugins ( ) { await this . #initPromise ; return this . #instances ! ; } async getGlobalScope ( ) { await this . #initPromise ; return this . #globalScope ! ; } async getBindings ( ) { await this . #initPromise ; return this . #bindings ! ; } async getModuleExports ( ) { await this . #initPromise ; return this . #moduleExports ! ; } async getMount ( name ) { await this . #initPromise ; return this . #mounts ! . get ( name ) ! ; } async dispatchFetch < WaitUntil extends any [ ] = unknown [ ] > ( input , init ? ) { await this . #initPromise ; let request = input instanceof Request && ! init ? input : new Request ( input , init ) ; const url = new URL ( request . url ) ; if ( this . #mounts ?. size ) { const mountMatch = this . #router ! . match ( url ) ; const name = this . #instances ! . CorePlugin . name ; if ( mountMatch !== null && mountMatch !== name ) { const mount = this . #mounts . get ( mountMatch ) ; if ( mount ) return mount . dispatchFetch ( request ) ; } } const { upstreamURL } = this . #instances ! . CorePlugin ; if ( upstreamURL && ! url . toString ( ) . startsWith ( upstreamURL . toString ( ) ) ) { let path = url . pathname + url . search ; if ( path . startsWith ( "/" ) ) path = path . substring ( 1 ) ; const newURL = new URL ( path , upstreamURL ) ; request = new Request ( newURL , request ) ; } const requestDepth = ( parseInt ( request . headers . get ( _kLoopHeader ) ! ) || 0 ) + 1 ; request . headers . delete ( _kLoopHeader ) ; return new RequestContext ( { requestDepth , pipelineDepth : 1 , } ) . runWith ( ( ) => this [ kDispatchFetch ] ( request , ! ! upstreamURL ) ) ; } async [ kDispatchFetch ] < WaitUntil extends any [ ] = unknown [ ] > ( request , proxy ) { await this . #initPromise ; if ( ! this . #compat ! . isEnabled ( "formdata_parser_supports_files" ) ) { request = withStringFormDataFiles ( request ) ; } request = withImmutableHeaders ( request ) ; return this . #globalScope ! [ kDispatchFetch ] < WaitUntil > ( request , proxy ) ; } async dispatchScheduled < WaitUntil extends any [ ] = unknown [ ] > ( scheduledTime ? , cron ? ) { await this . #initPromise ; const globalScope = this . #globalScope ; return new RequestContext ( ) . runWith ( ( ) => globalScope ! [ kDispatchScheduled ] < WaitUntil > ( scheduledTime , cron ) ) ; } async dispose ( ) { for ( const [ name ] of this . #plugins ) { const instance = this . #instances ?. [ name ] ; if ( instance ?. dispose ) { this . #ctx . log . verbose ( `- dispose(${ name })` ) ; await instance . dispose ( ) ; } } this . #watcher ?. dispose ( ) ; if ( this . #mounts ) { for ( const [ name , mount ] of this . #mounts ) { this . #ctx . log . debug ( `Unmounting \"${ name }\"...` ) ; await mount . dispose ( ) ; } this . #mounts . clear ( ) ; } } }