/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { IdleValue } from '../../../base/common/async.js'; import { illegalState } from '../../../base/common/errors.js'; import { SyncDescriptor } from './descriptors.js'; import { Graph } from './graph.js'; import { IInstantiationService, _util } from './instantiation.js'; import { ServiceCollection } from './serviceCollection.js'; // TRACING const _enableTracing = false; class CyclicDependencyError extends Error { constructor(graph) { var _a; super('cyclic dependency between services'); this.message = (_a = graph.findCycleSlow()) !== null && _a !== void 0 ? _a : `UNABLE to detect cycle, dumping graph: \n${graph.toString()}`; } } export class InstantiationService { constructor(services = new ServiceCollection(), strict = false, parent) { this._activeInstantiations = new Set(); this._services = services; this._strict = strict; this._parent = parent; this._services.set(IInstantiationService, this); } createChild(services) { return new InstantiationService(services, this._strict, this); } invokeFunction(fn, ...args) { let _trace = Trace.traceInvocation(fn); let _done = false; try { const accessor = { get: (id) => { if (_done) { throw illegalState('service accessor is only valid during the invocation of its target method'); } const result = this._getOrCreateServiceInstance(id, _trace); if (!result) { throw new Error(`[invokeFunction] unknown service '${id}'`); } return result; } }; return fn(accessor, ...args); } finally { _done = true; _trace.stop(); } } createInstance(ctorOrDescriptor, ...rest) { let _trace; let result; if (ctorOrDescriptor instanceof SyncDescriptor) { _trace = Trace.traceCreation(ctorOrDescriptor.ctor); result = this._createInstance(ctorOrDescriptor.ctor, ctorOrDescriptor.staticArguments.concat(rest), _trace); } else { _trace = Trace.traceCreation(ctorOrDescriptor); result = this._createInstance(ctorOrDescriptor, rest, _trace); } _trace.stop(); return result; } _createInstance(ctor, args = [], _trace) { // arguments defined by service decorators let serviceDependencies = _util.getServiceDependencies(ctor).sort((a, b) => a.index - b.index); let serviceArgs = []; for (const dependency of serviceDependencies) { let service = this._getOrCreateServiceInstance(dependency.id, _trace); if (!service && this._strict && !dependency.optional) { throw new Error(`[createInstance] ${ctor.name} depends on UNKNOWN service ${dependency.id}.`); } serviceArgs.push(service); } let firstServiceArgPos = serviceDependencies.length > 0 ? serviceDependencies[0].index : args.length; // check for argument mismatches, adjust static args if needed if (args.length !== firstServiceArgPos) { console.warn(`[createInstance] First service dependency of ${ctor.name} at position ${firstServiceArgPos + 1} conflicts with ${args.length} static arguments`); let delta = firstServiceArgPos - args.length; if (delta > 0) { args = args.concat(new Array(delta)); } else { args = args.slice(0, firstServiceArgPos); } } // now create the instance return new ctor(...[...args, ...serviceArgs]); } _setServiceInstance(id, instance) { 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'); } } _getServiceInstanceOrDescriptor(id) { let instanceOrDesc = this._services.get(id); if (!instanceOrDesc && this._parent) { return this._parent._getServiceInstanceOrDescriptor(id); } else { return instanceOrDesc; } } _getOrCreateServiceInstance(id, _trace) { let thing = this._getServiceInstanceOrDescriptor(id); if (thing instanceof SyncDescriptor) { return this._safeCreateAndCacheServiceInstance(id, thing, _trace.branch(id, true)); } else { _trace.branch(id, false); return thing; } } _safeCreateAndCacheServiceInstance(id, desc, _trace) { 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(id, desc, _trace) { const graph = new Graph(data => data.id.toString()); let cycleCount = 0; const stack = [{ id, desc, _trace }]; 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 (let dependency of _util.getServiceDependencies(item.desc.ctor)) { let instanceOrDesc = this._getServiceInstanceOrDescriptor(dependency.id); if (!instanceOrDesc && !dependency.optional) { console.warn(`[createInstance] ${id} depends on ${dependency.id} which is NOT registered.`); } if (instanceOrDesc instanceof SyncDescriptor) { const d = { id: dependency.id, desc: instanceOrDesc, _trace: item._trace.branch(dependency.id, true) }; 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, data._trace); this._setServiceInstance(data.id, instance); } graph.removeNode(data); } } return this._getServiceInstanceOrDescriptor(id); } _createServiceInstanceWithOwner(id, ctor, args = [], supportsDelayedInstantiation, _trace) { if (this._services.get(id) instanceof SyncDescriptor) { return this._createServiceInstance(ctor, args, supportsDelayedInstantiation, _trace); } else if (this._parent) { return this._parent._createServiceInstanceWithOwner(id, ctor, args, supportsDelayedInstantiation, _trace); } else { throw new Error(`illegalState - creating UNKNOWN service instance ${ctor.name}`); } } _createServiceInstance(ctor, args = [], _supportsDelayedInstantiation, _trace) { if (!_supportsDelayedInstantiation) { // eager instantiation return this._createInstance(ctor, args, _trace); } else { // 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 const idle = new IdleValue(() => this._createInstance(ctor, args, _trace)); return new Proxy(Object.create(null), { get(target, key) { if (key in target) { return target[key]; } let obj = idle.value; let prop = obj[key]; if (typeof prop !== 'function') { return prop; } prop = prop.bind(obj); target[key] = prop; return prop; }, set(_target, p, value) { idle.value[p] = value; return true; } }); } } } class Trace { constructor(type, name) { this.type = type; this.name = name; this._start = Date.now(); this._dep = []; } static traceInvocation(ctor) { return !_enableTracing ? Trace._None : new Trace(1 /* Invocation */, ctor.name || ctor.toString().substring(0, 42).replace(/\n/g, '')); } static traceCreation(ctor) { return !_enableTracing ? Trace._None : new Trace(0 /* Creation */, ctor.name); } branch(id, first) { let child = new Trace(2 /* Branch */, id.toString()); this._dep.push([id, first, child]); return child; } stop() { let dur = Date.now() - this._start; Trace._totals += dur; let causedCreation = false; function printChild(n, trace) { let res = []; let prefix = new Array(n + 1).join('\t'); for (const [id, first, child] of trace._dep) { if (first && child) { causedCreation = true; res.push(`${prefix}CREATES -> ${id}`); let nested = printChild(n + 1, child); if (nested) { res.push(nested); } } else { res.push(`${prefix}uses -> ${id}`); } } return res.join('\n'); } let lines = [ `${this.type === 0 /* Creation */ ? 'CREATE' : 'CALL'} ${this.name}`, `${printChild(1, this)}`, `DONE, took ${dur.toFixed(2)}ms (grand total ${Trace._totals.toFixed(2)}ms)` ]; if (dur > 2 || causedCreation) { console.log(lines.join('\n')); } } } Trace._None = new class extends Trace { constructor() { super(-1, null); } stop() { } branch() { return this; } }; Trace._totals = 0; //#endregion