@21epub/epub-thirdparty
Version:
epub-thirdparty
285 lines (284 loc) • 11.9 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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, optional, _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, isOptional) => {
if (_done) {
throw illegalState('service accessor is only valid during the invocation of its target method');
}
const result = this._getOrCreateServiceInstance(id, _trace);
if (!result && isOptional !== optional) {
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