import assert from "assert" ; import { Context , Mount , Option , OptionType , Plugin , PluginContext , SetupResult , StorageFactory , resolveStoragePersist , } from "@miniflare/shared" ; import { DurableObjectError } from "./error" ; import { DurableObjectConstructor , DurableObjectFactory , DurableObjectId , DurableObjectNamespace , DurableObjectState , kInstance , kObjectName , } from "./namespace" ; import { DurableObjectStorage } from "./storage" ; export type DurableObjectsObjectsOptions = Record < string , string | { className : string ; scriptName ? : string ; } > ; export interface DurableObjectsOptions { durableObjects ? : DurableObjectsObjectsOptions ; durableObjectsPersist ? : boolean | string ; } interface ProcessedDurableObject { name : string ; className : string ; scriptName ? : string ; } export class DurableObjectsPlugin extends Plugin < DurableObjectsOptions > implements DurableObjectsOptions { @ Option ( { type : OptionType . OBJECT , typeFormat : "NAME=CLASS[@MOUNT]" , name : "do" , alias : "o" , description : "Durable Object to bind" , fromEntries : ( entries ) => Object . fromEntries ( entries . map ( ( [ name , classScriptName ] ) => { const atIndex = classScriptName . lastIndexOf ( "@" ) ; if ( atIndex === - 1 ) { return [ name , classScriptName ] ; } else { const className = classScriptName . substring ( 0 , atIndex ) ; const scriptName = classScriptName . substring ( atIndex + 1 ) ; return [ name , { className , scriptName } ] ; } } ) ) , fromWrangler : ( { durable_objects } ) => durable_objects ?. bindings ?. reduce ( ( objects , { name , class_name , script_name } ) => { objects [ name ] = { className : class_name , scriptName : script_name } ; return objects ; } , { } as DurableObjectsObjectsOptions ) , } ) durableObjects ? : DurableObjectsObjectsOptions ; @ Option ( { type : OptionType . BOOLEAN_STRING , name : "do-persist" , description : "Persist Durable Object data (to optional path)" , logName : "Durable Objects Persistence" , fromWrangler : ( { miniflare } ) => miniflare ?. durable_objects_persist , } ) durableObjectsPersist ? : boolean | string ; readonly #persist ? : boolean | string ; readonly #processedObjects : ProcessedDurableObject [ ] ; readonly #requireFullUrl : boolean ; #contextPromise ? : Promise < void > ; #contextResolve ? : ( ) => void ; #constructors = new Map < string , DurableObjectConstructor > ( ) ; #bindings : Context = { } ; readonly #objects = new Map < string , Promise < DurableObjectState > > ( ) ; constructor ( ctx , options ? ) { super ( ctx ) ; this . assignOptions ( options ) ; this . #persist = resolveStoragePersist ( ctx . rootPath , this . durableObjectsPersist ) ; this . #processedObjects = Object . entries ( this . durableObjects ?? { } ) . map ( ( [ name , options ] ) => { const className = typeof options === "object" ? options . className : options ; const scriptName = typeof options === "object" ? options . scriptName : undefined ; return { name , className , scriptName } ; } ) ; this . #requireFullUrl = ctx . compat . isEnabled ( "durable_object_fetch_requires_full_url" ) ; } async getObject ( storage , id ) { assert ( this . #contextPromise , "beforeReload() must be called before getObject()" ) ; await this . #contextPromise ; const objectName = id [ kObjectName ] ; const key = `${ objectName }:${ id . toString ( ) }` ; let statePromise = this . #objects . get ( key ) ; if ( statePromise ) return statePromise ; statePromise = ( async ( ) => { const objectStorage = new DurableObjectStorage ( await storage . storage ( key , this . #persist ) ) ; const state = new DurableObjectState ( id , objectStorage ) ; const constructor = this . #constructors . get ( objectName ) ; assert ( constructor ) ; state [ kInstance ] = new constructor ( state , this . #bindings ) ; return state ; } ) ( ) ; this . #objects . set ( key , statePromise ) ; return statePromise ; } getNamespace ( storage , objectName ) { const factory = ( id ) => this . getObject ( storage , id ) ; return new DurableObjectNamespace ( objectName , factory , this . ctx ) ; } setup ( storageFactory ) { const bindings = { } ; for ( const { name } of this . #processedObjects ) { bindings [ name ] = this . getNamespace ( storageFactory , name ) ; } return { bindings , requiresModuleExports : this . #processedObjects . length > 0 , } ; } beforeReload ( ) { this . #objects . clear ( ) ; this . #contextPromise = new Promise ( ( resolve ) => ( this . #contextResolve = resolve ) ) ; } reload ( bindings , moduleExports , mounts ) { this . #constructors . clear ( ) ; for ( const { name , className , scriptName } of this . #processedObjects ) { let constructor ; if ( scriptName === undefined ) { constructor = moduleExports [ className ] ; } else { const scriptExports = mounts . get ( scriptName ) ?. moduleExports ; if ( ! scriptExports ) { throw new DurableObjectError ( "ERR_SCRIPT_NOT_FOUND" , `Script "${ scriptName }" for Durable Object "${ name }" not found` ) ; } constructor = scriptExports [ className ] ; } if ( constructor ) { this . #constructors . set ( name , constructor ) ; } else { const script = scriptName ? ` in script "${ scriptName }"` : "" ; throw new DurableObjectError ( "ERR_CLASS_NOT_FOUND" , `Class "${ className }"${ script } for Durable Object "${ name }" not found` ) ; } } this . #bindings = bindings ; assert ( this . #contextResolve , "beforeReload() must be called before reload()" ) ; this . #contextResolve ( ) ; } dispose ( ) { return this . beforeReload ( ) ; } }