Skip to content

VS Code: IoC Container and Dependency Injection #36

@xwcoder

Description

@xwcoder

VS Code: IoC Container and Dependency Injection

The brief introduction of VS Code IoC container

  • The source code is at src/vs/platform/instantiation/common, includes 506 lines code
  • Implements using TypeScript Decorator and Reflect.construct
  • Implements Dependency Injection , constructor injection method.
  • Implements an simple Dependency Lookup interface called invokeFunction.

The brief introduction of usage

  • Conventionally, the dependent object is called a service

  • In the IoC container, all services are singletons.

  • A service definition is two parts:

    1. the interface of a service (a practice of Dependence Inversion Principle - DIP)
    2. a service identifier, which is a decorator
  • Code example

    // 1. Define the service interface
    export interface IDBService {
      select(id: string): Promise<any[]>
    }
    
    // 2. Create the decorator
    import { createDecorator } from 'vs/platform/instantiation/common/instantiation'
    export const IDBService = createDecorator<IDBService>('dbService')
    
    // Declaration Merging
    // 3. Implement the service
    export class MysqlService implements IDBService {
      select(id: string): Promise<any[]> {
        console.log('mysql sql:', id)
        return Promise.resolve([])
      }
    }
    
    // 4. Use the service in client
    export class BookService {
      constructor(
        private name: string,
        @IDBService private dbService: IDBService
      ) {
        console.log('dbService:', dbService)
      }
      getBook() {
        console.log('name:', this.name)
        return this.dbService.select('select * from book')
      }
    }
    
    // 5. Init the IoC container
    import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'
    import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'
    
    const services = new ServiceCollection()
    // key is decorator
    services.set(IDBService, new MysqlService())
    
    // InstantiationService is the core of the IoC container
    const instantiationService = new InstantiationService(services)
    
    // 6. Create instance using instantiationService
    instantiationService.createInstance(BookService, 'hello book').getBook()

Implement from scratch

The overall process

  • Use decorator as service identifier, not the service itself.

  • Mark injection using decorator

    export class BookService {
      constructor(
        private name: string,
        @IDBService private dbService: IDBService,
        @ICacheService cacheService: ICacheService
      ) {
        console.log('dbService:', dbService)
      }
    }
  • Collect dependencies of service to be a array like form, such as [decorator1, decorator2, …]

  • Centrally store decorator service pairs, it’s a map like form, such as{ decorator1: service1, decorator2: service2, …}. The value stored is either a instance of Service or Service itself (actually a wrapper of Service class which will be instantiated when needed). The store is called serviceCollection here.

  • The main process of createInstance(Client) is:

    1. Get all the dependencies of Client. A array like of decorators, [decorator1, decorator2, …]
    2. Get all the instances of the dependencies from serviceCollection using decorators as key. If the value is a instance, return it. If the value is a wrapper, then create an instance using the wrapper and replaced the wrapper with the created instance.
    3. Create a instance of the Client using Reflect.construct and all dependent instances. Reflect.construct(Client, [param1, param2, ..., instance1, instance2, ...])

Collect dependencies of service using decorator

  • As the identifier, the decorator type is:

    // vs/platform/instantiation/common/instantiation.ts
    export interface ServiceIdentifier<T> {
    	(...args: any[]): void;
    	type: T;
    }
  • The Parameter Decorator is applied to the function for a class constructor or method declaration, it will be called at runtime with three arguments:

    1. Either the constructor function of the class for a static member, or the prototype of the class for an instance member.
    2. The name of the member.
    3. The ordinal index of the parameter in the function’s parameter list.
  • In the decorator, we can add an array property to the constructor function with a special name for storing all the dependencies, here is ‘$di$dependencies’ . Then put the current decorator and index in the array. When getting the dependencies of a service, just return the constructor’s $di$dependencies property. To avoid redefining the decorator, we use a map to store the decorator which key is a string id passed to the createDecorator as parameter.

    • Core code

      // vs/platform/instantiation/common/instantiation.ts
      
      export namespace _util {
      	export const serviceIds = new Map<string, ServiceIdentifier<any>>();
      
      	export const DI_TARGET = '$di$target';
      	export const DI_DEPENDENCIES = '$di$dependencies';
      
      	export function getServiceDependencies(ctor: any): { id: ServiceIdentifier<any>; index: number }[] {
      		return ctor[DI_DEPENDENCIES] || [];
      	}
      }
      
      export function createDecorator<T>(serviceId: string): ServiceIdentifier<T> {
      	if (_util.serviceIds.has(serviceId)) {
      		return _util.serviceIds.get(serviceId)!;
      	}
      
      	const id = <any>function (target: Function, key: string, index: number): any {
      		if (arguments.length !== 3) {
      			throw new Error('@IServiceName-decorator can only be used to decorate a parameter');
      		}
      		storeServiceDependency(id, target, index);
      	};
      
      	id.toString = () => serviceId;
      
      	_util.serviceIds.set(serviceId, id);
      	return id;
      }
      
      function storeServiceDependency(id: Function, target: Function, index: number): void {
      	if ((target as any)[_util.DI_TARGET] === target) {
      		(target as any)[_util.DI_DEPENDENCIES].push({ id, index });
      	} else {
      		(target as any)[_util.DI_DEPENDENCIES] = [{ id, index }];
      		(target as any)[_util.DI_TARGET] = target;
      	}
      }

      Notice that a toString method which returns the serviceId is added to the decorator: id.toString = () => serviceId . This method is used as a hash function in graph structure implementation that will be detailed later.

    • Example code

      import { IDBService } from './service/db'
      import { ICacheService } from './service/cache'
      
      class BookService {
        constructor(
          private name: string,
          @IDBService dbService: IDBService,
          @ICacheService cacheService: ICacheService
        ) {
        }
      }
      
      // result
      BookService.$di$dependencies = [{ id: IDBService, index: 1}, { id: ICacheService, index: 2}]

Implement the centrally store ServiceCollection

  • As mentioned before, we use a map to centrally store decorator service pairs. private _entries = new Map<ServiceIdentifier<any>, any>();. The value stored is either a instance of the service or a wrapper of the service, the wrapper is used to store the service constructor and other information like static parameters.

    • The wrapper definition

      // vs/platform/instantiation/common/descriptors.ts
      
      export class SyncDescriptor<T> {
      
      	readonly ctor: any;
      	readonly staticArguments: any[]; // 非依赖注入的参数
        
        // 是否延迟实例化
        // 为true时, 被注入时不进行实例化,而是返回一个Proxy 等使用时再实例化
      	readonly supportsDelayedInstantiation: boolean;
      
      	constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
      		this.ctor = ctor;
      		this.staticArguments = staticArguments;
      		this.supportsDelayedInstantiation = supportsDelayedInstantiation;
      	}
      }
  • As a store, ServiceCollection includes three methods: get, set, has

    • Core code

      // vs/platform/instantiation/common/serviceCollection.ts
      
      import { ServiceIdentifier } from 'vs/platform/instantiation/common/instantiation';
      import { SyncDescriptor } from './descriptors';
      
      export class ServiceCollection {
      
      	private _entries = new Map<ServiceIdentifier<any>, any>();
      
      	constructor(...entries: [ServiceIdentifier<any>, any][]) {
      		for (const [id, service] of entries) {
      			this.set(id, service);
      		}
      	}
      
      	set<T>(id: ServiceIdentifier<T>, instanceOrDescriptor: T | SyncDescriptor<T>): T | SyncDescriptor<T> {
      		const result = this._entries.get(id);
      		this._entries.set(id, instanceOrDescriptor);
      		return result;
      	}
      
      	has(id: ServiceIdentifier<any>): boolean {
      		return this._entries.has(id);
      	}
      
      	get<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
      		return this._entries.get(id);
      	}
      }

Implement the core of the IoC container InstantiationService

I will remove some unimportant code from source code in this part.

  • The InstantiationService is the core of the IoC container.
    • A InstantiationService instance is associated with a ServiceCollection instance.

    • The main function is createInstance

    • It implements a method called invokeFunction to provide Dependency Lookup

    • The IoC containers are layered, the InstantiationService implements a method call createChild to create a child InstantiationService .

      • The child InstantiationService has a reference to the parent InstantiationService, but the parent InstantiationService has no reference to the child InstantiationService .
      • So when the service is not present in the child InstantiationService, the child InstantiationService will recursively look up from the ancestor InstantiationService
    • The interface of InstantiationService

      // vs/platform/instantiation/common/instantiation.ts
      
      export interface IInstantiationService {
      	createInstance<T>(descriptor: descriptors.SyncDescriptor0<T>): T;
      	createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
      
      	invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R;
      
      	createChild(services: ServiceCollection): IInstantiationService;
      }

The Constructor

  • The constructor accepts a ServiceCollection instance and an optional parent InstantiationService instance.

  • TheInstantiationService is also a service, so this will be stored in the ServiceCollection instance passed to the constructor.

  • The core code

    // vs/platform/instantiation/common/instantiation.ts
    export const IInstantiationService = createDecorator<IInstantiationService>('instantiationService');
    // vs/platform/instantiation/common/instantiationService.ts
    
    import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'
    
    export class InstantiationService implements IInstantiationService {
    	constructor(
    		private readonly _services: ServiceCollection = new ServiceCollection(),
    		private readonly _parent?: InstantiationService
    	) {
    		this._services.set(IInstantiationService, this);
    	}

The createChild Method

Create a new InstantiationService with this as parent, and return it.

  • Code

    // vs/platform/instantiation/common/instantiationService.ts
    
    createChild(services: ServiceCollection): IInstantiationService {
    		return new InstantiationService(services, this);
    }

The createInstance Method

The createInstance Method is the core of the IoC container. Its main function is to instantiate the services.

instantiationService.createInstance(BookService, 'hello booke').getBook()

If there is only one layer of dependency, like the previous example: BookService dependents IDBService and ICacheService, IDBService and ICacheService have no dependency. The instantiation process is simple and clear:

  • Get the dependence list from Constructor[$di$dependencies] . BookService[$di$dependencies]

  • Get all the dependency services from serviceCollection and instantiate them if they have not been instantiated. Assume serviceCollection stores the SyncDescriptor of them.

    // 1. Get the DBService Descriptor and instantiate DBService.
    const dbServiceDescriptor = this._services.get(IDBService)
    const dbService = Reflect.construct(dbServiceDescriptor.ctor, dbServiceDescriptor.staticArguments)
    
    // 2. Get the CacheService Descriptor and instantiate CacheService
    const cacheServiceDescriptor = this._services.get(ICacheService)
    const cacheService = Reflect.construct(cacheServiceDescriptor.ctor, cacheServiceDescriptor.staticArguments)
  • Instantiate the BookService using Reflect.construct.

    Reflect.construct(BookService, ['hello booke', dbService, cacheService])

If there are multiple layers, perform this process recursively starting from leaf services (A leaf service means a service has no dependency ). So, the brief common algorithm includes two steps:

  • Construct the dependency graph.

    IMAGE

    Suppose we want to instantiate theclient which depends on 1. We get the dependency graph as the picture showed, 1 depends on 2 and 3, 2 depends on 4 and 6, 3 depends on 4 and 5, 4 depends on 5 and 6, 5 depends 6 and 7, 6 and 7 have no dependence. Client is not in the graph.

  • Perform instantiation recursively starting from leaf services.

    1. 6 and 7 are leaf services, instantiate them and remove them from the graph.
    2. Now, 5 is leaf service, instantiate it and remove it from the graph.
    3. Now, 4 is leaf service, instantiate it and remove it from the graph.
    4. Now, 2 and 3 are leaf services, instantiate them and remove them from the graph.
    5. Now, 1 is leaf service, instantiate it and remove it from the graph.
    6. Now, the graph is empty, then we instantiate the client.

Now let’s look at the algorithm implementation in detail.

Implement the Graph structure

  • Using a Orthogonal linked list like structure to store Graph:

    • Every Node has two map properties, one is used to store the nodes that depend on the current node called incoming, the other is used to store the nodes that the current node depends on called outgoing. That means incoming and outgoing store the edges.

      // vs/platform/instantiation/common/graph.ts
      export class Node<T> {
      	readonly incoming = new Map<string, Node<T>>();
      	readonly outgoing = new Map<string, Node<T>>();
      
      	constructor(
      		readonly key: string,
      		readonly data: T
      	) { }
      }
    • The Graph class uses a map to stores all the nodes.

    // vs/platform/instantiation/common/graph.ts
    export class Graph<T> {
    	private readonly _nodes = new Map<string, Node<T>>();
    
    	constructor(private readonly _hashFn: (element: T) => string) {
    		// empty
    	}
    }

    The constructor accepts a hash function which is used to generate a string as the map key . As mentioned before, the IoC container uses decorator.toString as the hash function.

    // vs/platform/instantiation/common/instantiationService.ts
    const graph = new Graph<Triple>(data => data.id.toString());
  • The Graph class provides a lookupOrInsertNode method: returns the node if it already exists, otherwise create the node and add it to the map first.

    // vs/platform/instantiation/common/graph.ts
    lookupOrInsertNode(data: T): Node<T> {
    		const key = this._hashFn(data);
    		let node = this._nodes.get(key);
    
    		if (!node) {
    			node = new Node(key, data);
    			this._nodes.set(key, node);
    		}
    
    		return node;
    	}
  • The Graph class provides a insertEdge method for inserting an edge. Lookup or create the from and theto Node first, then insert them is each other’s outgoing and incoming.

    // vs/platform/instantiation/common/graph.ts
    insertEdge(from: T, to: T): void {
    		const fromNode = this.lookupOrInsertNode(from);
    		const toNode = this.lookupOrInsertNode(to);
    
    		fromNode.outgoing.set(toNode.key, toNode);
    		toNode.incoming.set(fromNode.key, fromNode);
    }
  • The Graph class provides a removeNode method for removing one node from the graph.

    // vs/platform/instantiation/common/graph.ts
    removeNode(data: T): void {
    		const key = this._hashFn(data);
    		this._nodes.delete(key);
    		for (const node of this._nodes.values()) {
    			node.outgoing.delete(key);
    			node.incoming.delete(key);
    		}
    }
  • The Graph class provides a roots() method returns all the leaf nodes. A leaf node is a node who’s outgoing size is 0, that means it has no dependence. I think the method name is inverse, but we still think them as leaf nodes in this post.

    // vs/platform/instantiation/common/graph.ts
    
    roots(): Node<T>[] {
    		const ret: Node<T>[] = [];
    		for (const node of this._nodes.values()) {
    			if (node.outgoing.size === 0) {
    				ret.push(node);
    			}
    		}
    		return ret;
    	}
  • The Graph class provides a findCycleSlow to find cycle in the graph using DFS. This method is slow, so it is only used to display the cycle information when there is a cycle. The IoC container uses other lightweight ways to detect cycle based on some assumptions.

    // vs/platform/instantiation/common/graph.ts
    
    findCycleSlow() {
    		for (const [id, node] of this._nodes) {
    			const seen = new Set<string>([id]);
    			const res = this._findCycle(node, seen);
    			if (res) {
    				return res;
    			}
    		}
    		return undefined;
    	}
    
    	private _findCycle(node: Node<T>, seen: Set<string>): string | undefined {
    		for (const [id, outgoing] of node.outgoing) {
    			if (seen.has(id)) {
    				return [...seen, id].join(' -> ');
    			}
    			seen.add(id);
    			const value = this._findCycle(outgoing, seen);
    			if (value) {
    				return value;
    			}
    			seen.delete(id);
    		}
    		return undefined;
    	}

Implement the createInstance algorithm

The createInstance is a override method.

// vs/platform/instantiation/common/instantiationService.ts
createInstance<T>(descriptor: SyncDescriptor0<T>): T;
createInstance<Ctor extends new (...args: any[]) => any, R extends InstanceType<Ctor>>(ctor: Ctor, ...args: GetLeadingNonServiceArgs<ConstructorParameters<Ctor>>): R;
createInstance(ctorOrDescriptor: any | SyncDescriptor<any>, ...rest: any[]): any {
	let result: any;
	if (ctorOrDescriptor instanceof SyncDescriptor) {
		result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest));
	} else {
		result = this._createInstance(ctorOrDescriptor, rest);
	}
	return result;
}

It accepts a constructor or a SyncDescriptor.

// vs/platform/instantiation/common/instantiationService.ts
private _createInstance<T>(ctor: any, args: any[] = []): T {

  // 1. get all dependence identifiers from ctor['$di$dependencies']
  const serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index);
  const serviceArgs: any[] = [];

  // 2. get all dependence instances
  for (const dependency of serviceDependencies) {
    const service = this._getOrCreateServiceInstance(dependency.id);
    if (!service) {
      this._throwIfStrict(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`, false);
    }
    serviceArgs.push(service);
  }

  const firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length;

  // 3. Correction parameter sequence
  if (args.length !== firstServiceArgPos) {
    console.trace(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`);

    const delta = firstServiceArgPos - args.length;
    if (delta > 0) {
      args = args.concat(new Array(delta));
    } else {
      args = args.slice(0, firstServiceArgPos);
    }
  }

  // 4. now create the instance
  return Reflect.construct<any, T>(ctor, args.concat(serviceArgs));
}
  • The main logic of _createInstance:
    1. Get all dependence identifiers from constructor[’$di$dependencies’]
    2. Get all dependence instances, if the dependence is already instantiated return it, otherwise instantiate it first. this._getOrCreateServiceInstance(dependency.id)
    3. Now, we have all the dependence instances and other arguments, correct the arguments order.
    4. Instantiate the ctor using the Reflect.construct API.
  • The logic of _getOrCreateServiceInstance is very simple:
    1. Look up the value recursively from serviceCollection store using decorator as key.
    2. If the value is a SyncDescriptor then instantiate and return it, otherwise the value is an instance already, just return it.
// vs/platform/instantiation/common/instantiationService.ts
protected _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>): T {
  const thing = this._getServiceInstanceOrDescriptor(id);
  if (thing instanceof SyncDescriptor) {
    return this._safeCreateAndCacheServiceInstance(id, thing);
  } else {
    return thing;
  }
}

private _getServiceInstanceOrDescriptor<T>(id: ServiceIdentifier<T>): T | SyncDescriptor<T> {
  const instanceOrDesc = this._services.get(id);
  if (!instanceOrDesc && this._parent) {
    return this._parent._getServiceInstanceOrDescriptor(id);
  } else {
    return instanceOrDesc;
  }
}

_safeCreateAndCacheServiceInstance saves the instantiating service in a Set, if there is duplicated instantiating service, there is a cycle.

// vs/platform/instantiation/common/instantiationService.ts
private readonly _activeInstantiations = new Set<ServiceIdentifier<any>>();

private _safeCreateAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>, _trace: Trace): T {
  if (this._activeInstantiations.has(id)) {
    throw new Error(`illegal state - RECURSIVELY instantiating service '${id}'`);
  }
  this._activeInstantiations.add(id);
  try {
    return this._createAndCacheServiceInstance(id, desc, _trace);
  } finally {
    this._activeInstantiations.delete(id);
  }
}

_createAndCacheServiceInstance is the main instance algorithm.

// vs/platform/instantiation/common/instantiationService.ts
private _createAndCacheServiceInstance<T>(id: ServiceIdentifier<T>, desc: SyncDescriptor<T>): T {

  type Triple = { id: ServiceIdentifier<any>; desc: SyncDescriptor<any>;};
  const graph = new Graph<Triple>(data => data.id.toString());

  let cycleCount = 0;
  const stack = [{ id, desc }];
  while (stack.length) {
    const item = stack.pop()!;
    graph.lookupOrInsertNode(item);

    // a weak but working heuristic for cycle checks
    if (cycleCount++ > 1000) {
      throw new CyclicDependencyError(graph);
    }

    // check all dependencies for existence and if they need to be created first
    for (const dependency of _util.getServiceDependencies(item.desc.ctor)) {

      const instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id);
      if (!instanceOrDesc) {
        this._throwIfStrict(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`, true);
      }

      if (instanceOrDesc instanceof SyncDescriptor) {
        const d = { id: dependency.id, desc: instanceOrDesc };
        graph.insertEdge(item, d);
        stack.push(d);
      }
    }
  }

  while (true) {
    const roots = graph.roots();

    // if there is no more roots but still
    // nodes in the graph we have a cycle
    if (roots.length === 0) {
      if (!graph.isEmpty()) {
        throw new CyclicDependencyError(graph);
      }
      break;
    }

    for (const { data } of roots) {
      // Repeat the check for this still being a service sync descriptor. That's because
      // instantiating a dependency might have side-effect and recursively trigger instantiation
      // so that some dependencies are now fullfilled already.
      const instanceOrDesc = this._getServiceInstanceOrDescriptor(data.id);
      if (instanceOrDesc instanceof SyncDescriptor) {
        // create instance and overwrite the service collections
        const instance = this._createServiceInstanceWithOwner(data.id, data.desc.ctor, data.desc.staticArguments, data.desc.supportsDelayedInstantiation);
        this._setServiceInstance(data.id, instance);
      }
      graph.removeNode(data);
    }
  }
  return <T>this._getServiceInstanceOrDescriptor(id);
}

private _setServiceInstance<T>(id: ServiceIdentifier<T>, instance: T): void {
  if (this._services.get(id) instanceof SyncDescriptor) {
    this._services.set(id, instance);
  } else if (this._parent) {
    this._parent._setServiceInstance(id, instance);
  } else {
    throw new Error('illegalState - setting UNKNOWN service instance');
  }
}

There are two while loop.

  • The fist while loop is used to build the Graph using stack structure, the algorithm is routine and simple.
    1. Put the start element in the stack
    2. Pop one element named item from the stack, lookup or insert it into the graph using graph.lookupOrInsertNode(item).
    3. Get all the dependence of item.desc.ctor
    4. Traverse the dependencies, if the dependence is a SyncDescriptor push it into the stack and add edges in graph.
    5. Repeat step 2 from step 4.
    6. There is a assumption: If there are still elements in the stack after 1000 runs, we assume that there are cyclic dependencies.
  • The second while loop is used to instance all the services from leaf services
    1. Get all the leaf nodes using const roots = graph.roots()
    2. Traverse the roots, instantiates all the leaf services, puts the instances in the serviceCollection, removes the node from the graph.
    3. Repeat step 1 and 2.
    4. There is a cyclic check: If there is no leaf node but the graph is not empty, so there are cyclic dependencies.

If supportsDelayedInstantiation is false then create the instance, this._createInstance is called here and all the dependencies are instantiated already.

If supportsDelayedInstantiation is true then return a Proxy defines the get, set and getPrototypeOf traps, the service will be instantiated when actually used.

private _createServiceInstanceWithOwner<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean): T {
  if (this._services.get(id) instanceof SyncDescriptor) {
    return this._createServiceInstance(id, ctor, args, supportsDelayedInstantiation);
  } else if (this._parent) {
    return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation);
  } else {
    throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`);
  }
}

private _createServiceInstance<T>(id: ServiceIdentifier<T>, ctor: any, args: any[] = [], supportsDelayedInstantiation: boolean): T {
  if (!supportsDelayedInstantiation) {
    // eager instantiation
    return this._createInstance(ctor, args);

  } else {
    const child = new InstantiationService(undefined, this._strict, this, this._enableTracing);
    child._globalGraphImplicitDependency = String(id);

    // Return a proxy object that's backed by an idle value. That
    // strategy is to instantiate services in our idle time or when actually
    // needed but not when injected into a consumer

    // return "empty events" when the service isn't instantiated yet
    const earlyListeners = new Map<string, LinkedList<Parameters<Event<any>>>>();

    const idle = new IdleValue<any>(() => {
        const result = child._createInstance<T>(ctor, args);

        // early listeners that we kept are now being subscribed to
        // the real service
        for (const [key, values] of earlyListeners) {
          const candidate = <Event<any>>(<any>result)[key];
          if (typeof candidate === 'function') {
            for (const listener of values) {
              candidate.apply(result, listener);
            }
          }
        }
        earlyListeners.clear();

        return result;
    });
    return <T>new Proxy(Object.create(null), {
        get(target: any, key: PropertyKey): any {

          if (!idle.isInitialized) {
            // looks like an event
            if (typeof key === 'string' && (key.startsWith('onDid') || key.startsWith('onWill'))) {
              let list = earlyListeners.get(key);
              if (!list) {
                list = new LinkedList();
                earlyListeners.set(key, list);
              }

              const event: Event<any> = (callback, thisArg, disposables) => {
                const rm = list!.push([callback, thisArg, disposables]);
                return toDisposable(rm);
              };
              return event;
            }
          }

          // value already exists
          if (key in target) {
            return target[key];
          }

          // create value
          const obj = idle.value;
          let prop = obj[key];
          if (typeof prop !== 'function') {
            return prop;
          }
          prop = prop.bind(obj);
          target[key] = prop;
          return prop;
        },

        set(_target: T, p: PropertyKey, value: any): boolean {
          idle.value[p] = value;
          return true;
        },

        getPrototypeOf(_target: T) {
          return ctor.prototype;
        }
    });
  }
}

The invokeFunction Method

The IoC container provides a simple Dependency Lookup like behavior, the invokeFunction accepts a function, the function will be called immediately with an accessor as the first argument, you can use the accessor to lookup services.

// vs/platform/instantiation/common/instantiationService.ts
invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
  let _done = false;
  try {
    const accessor: ServicesAccessor = {
      get: <T>(id: ServiceIdentifier<T>) => {
        if (_done) {
          throw illegalState('service accessor is only valid during the invocation of its target method');
        }

        const result = this._getOrCreateServiceInstance(id);
        if (!result) {
          throw new Error(`[invokeFunction] unknown service '${id}'`);
        }
        return result;
      }
    };
    return fn(accessor, ...args);
  } finally {
    _done = true;
  }
}
instantiationService.invokeFunction((accessor) => {
  const dbService = accessor.get(IDBService)
	const cacheService = accessor.get(ICacheService)

   dbService.select(1)
   cacheService.get(1)
})

Implement the Singleton Service Register

The IoC container provides a additional singleton service register of the whole app.

// vs/platform/instantiation/common/extensions.ts
import { SyncDescriptor } from './descriptors';
import { BrandedService, ServiceIdentifier } from './instantiation';

const _registry: [ServiceIdentifier<any>, SyncDescriptor<any>][] = [];

export const enum InstantiationType {
	/**
	 * Instantiate this service as soon as a consumer depdends on it. _Note_ that this
	 * is more costly as some upfront work is done that is likely not needed
	 */
	Eager = 0,

	/**
	 * Instantiate this service as soon as a consumer uses it. This is the _better_
	 * way of registering a service.
	 */
	Delayed = 1
}

export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctor: new (...services: Services) => T, supportsDelayedInstantiation: InstantiationType): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, descriptor: SyncDescriptor<any>): void;
export function registerSingleton<T, Services extends BrandedService[]>(id: ServiceIdentifier<T>, ctorOrDescriptor: { new(...services: Services): T } | SyncDescriptor<any>, supportsDelayedInstantiation?: boolean | InstantiationType): void {
	if (!(ctorOrDescriptor instanceof SyncDescriptor)) {
		ctorOrDescriptor = new SyncDescriptor<T>(ctorOrDescriptor as new (...args: any[]) => T, [], Boolean(supportsDelayedInstantiation));
	}

	_registry.push([id, ctorOrDescriptor]);
}

export function getSingletonServiceDescriptors(): [ServiceIdentifier<any>, SyncDescriptor<any>][] {
	return _registry;
}

The registerSingleton() method stores the service of SyncDescriptor and decorator in the array, every element is a SyncDescriptor and decorator pair, the getSingletonServiceDescriptors() returns the array. The array is usually used to create ServiceCollection

Example usage code:

registerSingleton(IUserDataSyncLogService, UserDataSyncLogService, InstantiationType.Delayed);
registerSingleton(IIgnoredExtensionsManagementService, IgnoredExtensionsManagementService, InstantiationType.Delayed);
registerSingleton(IGlobalExtensionEnablementService, GlobalExtensionEnablementService, InstantiationType.Delayed);

// vs/workbench/api/common/extensionHostMain.ts
const services = new ServiceCollection(...getSingletonServiceDescriptors());

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions