import { flatten } from 'lodash' ; import { ShallowPromise } from '@osd/utility-types' ; import { pick } from '@osd/std' ; import type { CoreId , PluginOpaqueId } from '../server' ; /**
 * Make all properties in T optional, except for the properties whose keys are in the union K
 */ type PartialExceptFor < T , K extends keyof T > = Partial < T > & Pick < T , K > ; remarks param context param rest returns public export type IContextProvider < THandler extends HandlerFunction < any > , TContextName extends keyof HandlerContextType < THandler > > = ( context : PartialExceptFor < HandlerContextType < THandler > , 'core' > , ... rest : HandlerParameters < THandler > ) => Promise < HandlerContextType < THandler > [ TContextName ] > | HandlerContextType < THandler > [ TContextName ] ; /**
 * A function that accepts a context object and an optional number of additional arguments. Used for the generic types
 * in  IContextContainer public export type HandlerFunction < T extends object > = ( context : T , ... args : any [ ] ) => any ; /**
 * Extracts the type of the first argument of a  HandlerFunction to represent the type of the context.
 *
 *  public export type HandlerContextType < T extends HandlerFunction < any > > = T extends HandlerFunction < infer U > ? U : never ; /**
 * Extracts the types of the additional arguments of a  HandlerFunction , excluding the
 *  HandlerContextType .
 *
 *  public export type HandlerParameters < T extends HandlerFunction < any > > = T extends ( context : any , ... args : infer U ) => any ? U : never ; remarks A  IContextContainer can be used by any Core service or plugin (known as the "service owner") which wishes to
 * expose APIs in a handler function. The container object will manage registering context providers and configuring a
 * handler with all of the contexts that should be exposed to the handler's plugin. This is dependent on the
 * dependencies that the handler's plugin declares.
 *
 * Contexts providers are executed in the order they were registered. Each provider gets access to context values
 * provided by any plugins that it depends on.
 *
 * In order to configure a handler with context, you must call the  IContextContainer . createHandler function and
 * use the returned handler which will automatically build a context object when called.
 *
 * When registering context or creating handlers, the _calling plugin's opaque id_ must be provided. This id is passed
 * in via the plugin's initializer and can be accessed from the  PluginInitializerContext . opaqueId Note this
 * should NOT be the context service owner's id, but the plugin that is actually registering the context or handler.
 *
 * ```ts
 * // Correct
 * class MyPlugin {
 *   private readonly handlers = new Map();
 *
 *   setup(core) {
 *     this.contextContainer = core.context.createContextContainer();
 *     return {
 *       registerContext(pluginOpaqueId, contextName, provider) {
 *         this.contextContainer.registerContext(pluginOpaqueId, contextName, provider);
 *       },
 *       registerRoute(pluginOpaqueId, path, handler) {
 *         this.handlers.set(
 *           path,
 *           this.contextContainer.createHandler(pluginOpaqueId, handler)
 *         );
 *       }
 *     }
 *   }
 * }
 *
 * // Incorrect
 * class MyPlugin {
 *   private readonly handlers = new Map();
 *
 *   constructor(private readonly initContext: PluginInitializerContext) {}
 *
 *   setup(core) {
 *     this.contextContainer = core.context.createContextContainer();
 *     return {
 *       registerContext(contextName, provider) {
 *         // BUG!
 *         // This would leak this context to all handlers rather that only plugins that depend on the calling plugin.
 *         this.contextContainer.registerContext(this.initContext.opaqueId, contextName, provider);
 *       },
 *       registerRoute(path, handler) {
 *         this.handlers.set(
 *           path,
 *           // BUG!
 *           // This handler will not receive any contexts provided by other dependencies of the calling plugin.
 *           this.contextContainer.createHandler(this.initContext.opaqueId, handler)
 *         );
 *       }
 *     }
 *   }
 * }
 * ```
 *
 *  typeParam THandler - the type of  HandlerFunction this container should manage. The first argument of this
 *                       function will be used as the context type.
 *
 *  public export interface IContextContainer < THandler extends HandlerFunction < any > > { remarks param pluginOpaqueId param contextName param provider - A  IContextProvider to be called each time a new context is created.
     *  returns The  IContextContainer for method chaining.
      registerContext < TContextName extends keyof HandlerContextType < THandler > > ( pluginOpaqueId , contextName , provider ) : this ; param pluginOpaqueId param handler returns createHandler ( pluginOpaqueId , handler ) : ( ... rest : HandlerParameters < THandler > ) => ShallowPromise < ReturnType < THandler > > ; } internal export class ContextContainer < THandler extends HandlerFunction < any > > implements IContextContainer < THandler > { /**
     * Used to map contexts to their providers and associated plugin. In registration order which is tightly coupled to
     * plugin load order.
     */ private readonly contextProviders = new Map < keyof HandlerContextType < THandler > , { provider : IContextProvider < THandler , keyof HandlerContextType < THandler > > ; source : symbol ; } > ( ) ; /** Used to keep track of which plugins registered which contexts for dependency resolution. */ private readonly contextNamesBySource : Map < symbol , Array < keyof HandlerContextType < THandler > > > ; param pluginDependencies constructor ( private readonly pluginDependencies , private readonly coreId ) { this . contextNamesBySource = new Map < symbol , Array < keyof HandlerContextType < THandler > > > ( [ [ coreId , [ ] ] , ] ) ; } public registerContext = < TContextName extends keyof HandlerContextType < THandler > > ( source , contextName , provider ) : this => { if ( this . contextProviders . has ( contextName ) ) { throw new Error ( `Context provider for ${ contextName } has already been registered.` ) ; } if ( source !== this . coreId && ! this . pluginDependencies . has ( source ) ) { throw new Error ( `Cannot register context for unknown plugin: ${ source . toString ( ) }` ) ; } this . contextProviders . set ( contextName , { provider , source } ) ; this . contextNamesBySource . set ( source , [ ... ( this . contextNamesBySource . get ( source ) || [ ] ) , contextName , ] ) ; return this ; } ; public createHandler = ( source , handler ) => { if ( source !== this . coreId && ! this . pluginDependencies . has ( source ) ) { throw new Error ( `Cannot create handler for unknown plugin: ${ source . toString ( ) }` ) ; } return ( async ( ... args ) => { const context = await this . buildContext ( source , ... args ) ; return handler ( context , ... args ) ; } ) as ( ... args : HandlerParameters < THandler > ) => ShallowPromise < ReturnType < THandler > > ; } ; private async buildContext ( source , ... contextArgs ) { const contextsToBuild = new Set ( this . getContextNamesForSource ( source ) ) ; return [ ... this . contextProviders ] . sort ( sortByCoreFirst ( this . coreId ) ) . filter ( ( [ contextName ] ) => contextsToBuild . has ( contextName ) ) . reduce ( async ( contextPromise , [ contextName , { provider , source : providerSource } ] ) => { const resolvedContext = await contextPromise ; const exposedContext = pick ( resolvedContext , [ ... this . getContextNamesForSource ( providerSource ) , ] ) as PartialExceptFor < HandlerContextType < THandler > , 'core' > ; return { ... resolvedContext , [ contextName ] : await provider ( exposedContext , ... contextArgs ) , } ; } , Promise . resolve ( { } ) as Promise < HandlerContextType < THandler > > ) ; } private getContextNamesForSource ( source ) { if ( source === this . coreId ) { return this . getContextNamesForCore ( ) ; } else { return this . getContextNamesForPluginId ( source ) ; } } private getContextNamesForCore ( ) { return new Set ( this . contextNamesBySource . get ( this . coreId ) ! ) ; } private getContextNamesForPluginId ( pluginId ) { const pluginDeps = this . pluginDependencies . get ( pluginId ) ; if ( ! pluginDeps ) { throw new Error ( `Cannot create context for unknown plugin: ${ pluginId . toString ( ) }` ) ; } return new Set ( [ ... this . contextNamesBySource . get ( this . coreId ) ! , ... ( this . contextNamesBySource . get ( pluginId ) || [ ] ) , ... flatten ( pluginDeps . map ( ( p ) => this . contextNamesBySource . get ( p ) || [ ] ) ) , ] ) ; } } /** Sorts context provider pairs by core pairs first. */ const sortByCoreFirst = ( coreId ) : ( ( left : [ any , { source : symbol ; } ] , right : [ any , { source : symbol ; } ] ) => number ) => ( [ leftName , leftProvider ] , [ rightName , rightProvider ] ) => { if ( leftProvider . source === coreId ) { return rightProvider . source === coreId ? 0 : - 1 ; } else { return rightProvider . source === coreId ? 1 : 0 ; } } ;