class-injector
Version:
Crazy simple dependency injection for TypeScript
213 lines (212 loc) • 8.73 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isContext = exports.getContext = exports.provideRawInstance = exports.provideInstance = exports.ProvideInstance = exports.provide = exports.Provide = exports.Inject = exports.Context = exports.createContext = exports.createPartialForContext = exports.__InstanceRegistry = exports.__TypeRegistry = exports.__ContextSymbol = void 0;
require("reflect-metadata");
exports.__ContextSymbol = Symbol.for('@@class-injector-context@@');
class PartialContextImpl extends Map {
}
class ContextImpl {
constructor() {
this.contextPartials = [];
}
get(type) {
for (const partial of this.contextPartials) {
if (partial.has(type)) {
return partial.get(type);
}
}
return null;
}
set(type, instance) {
if (this.contextPartials.length === 0) {
throw new Error([
'Failed to set an instance in the context because no context partials are bound to the context.',
'Unless you are manipulating the context directly, this is a bug.',
'Please report this issue at https://github.com/bcheidemann/class-injector/issues/new'
].join('\n'));
}
this.contextPartials[0].set(type, instance);
}
has(type) {
return this.contextPartials.some(partial => partial.has(type));
}
instantiate(type) {
let instance = this.get(type);
if (!instance) {
// Get existing context if one exists
const existingContext = type.prototype[exports.__ContextSymbol];
if (existingContext) {
// Extend the existing context
existingContext.contextPartials.push(...this.contextPartials);
// Instantiate the type.
instance = new type();
// Remove the context partials we added
existingContext.contextPartials.splice(existingContext.contextPartials.length - this.contextPartials.length, this.contextPartials.length);
}
else {
// Bind the context to the types prototype
type.prototype[exports.__ContextSymbol] = this;
// Instantiate the type.
instance = new type();
// Unbind the prototype from the context.
type.prototype[exports.__ContextSymbol] = undefined;
}
// Bind the instance to the context.
this.bind(instance);
// Store the instance in the context.
this.set(type, instance);
}
return instance;
}
;
bind(instance) {
if (typeof instance !== 'object') {
return;
}
const existingContext = instance[exports.__ContextSymbol];
if (existingContext) {
// Extend the existing context
existingContext.contextPartials.push(...this.contextPartials);
return;
}
// Bind this context to the instance
Object.defineProperty(instance, exports.__ContextSymbol, {
value: this,
});
}
}
exports.__TypeRegistry = new Map();
exports.__InstanceRegistry = new Map();
function createPartialForContext(context, options = {}) {
// Create a new context
const partial = new PartialContextImpl();
// Provide the context to the constructor
const provide = options.provide || [];
for (const dependency of provide) {
let type;
let instance;
if (Array.isArray(dependency)) {
type = dependency[0];
instance = dependency[1];
}
else {
type = dependency.constructor;
instance = dependency;
}
// Add the instance to the context
partial.set(type, instance);
// Add the context to the instance
if (typeof instance === 'object') {
Object.defineProperty(instance, exports.__ContextSymbol, {
value: context,
});
}
}
return partial;
}
exports.createPartialForContext = createPartialForContext;
function createContext(options = {}) {
// Create a new context context
const context = new ContextImpl();
// Create a new context
const partial = createPartialForContext(context, options);
// Bind the context to the context
context.contextPartials.unshift(partial);
return context;
}
exports.createContext = createContext;
const Context = (options = {}) => (target) => {
// Get the context context from the target if it exists
let context = target.prototype[exports.__ContextSymbol];
// Create a new context context if it doesn't exist and bind it to the target
if (!context) {
context = new ContextImpl();
target.prototype[exports.__ContextSymbol] = context;
}
// Create a new context partial
const partial = createPartialForContext(context, options);
// Bind the context partial to the context
context.contextPartials.unshift(partial);
};
exports.Context = Context;
const Inject = (type) => (target, propertyKey) => {
// Get the type of the dependency to inject
const _type = (type === undefined)
? Reflect.getMetadata("design:type", target, propertyKey)
: type;
// Lazy bind the dependency to the instance via a getter
Object.defineProperty(target, propertyKey, {
get() {
// Get the context
let context = this[exports.__ContextSymbol];
if (!context) {
this[exports.__ContextSymbol] = context = createContext();
}
// Return the context if the @Inject(Context) pattern is used
if (_type === exports.Context) {
return context;
}
// Try to get the instance from the context
if (context.has(_type)) {
return this[exports.__ContextSymbol].get(_type);
}
// Get the type from the symbol registry
if (exports.__InstanceRegistry.has(_type)) {
const instance = exports.__InstanceRegistry.get(_type);
context.set(_type, instance);
// NOTE: The instance is not bound to the context since it could be reused across many
// contexts ProvideInstance and provideInstance should not be used if the instance requires
// access to the parent context. Provide and provide should be used instead, as these will
// create a new instance for each context and bind the context to that instance.
return instance;
}
if (exports.__TypeRegistry.has(_type)) {
const type = exports.__TypeRegistry.get(_type);
return context.instantiate(type);
}
// Ensure the dependency can be instantiated as a constructor type
if (typeof _type !== 'function') {
throw new Error('Cannot instantiate a symbol type. Please use a class type instead or provide an instance for the type in the context.');
}
return context.instantiate(_type);
},
});
};
exports.Inject = Inject;
const Provide = (symbol) => (target) => {
const _symbol = symbol === undefined ? target : symbol;
exports.__TypeRegistry.set(_symbol, target);
};
exports.Provide = Provide;
function provide(symbolOrType, type) {
const _type = type === undefined ? symbolOrType : type;
exports.__TypeRegistry.set(symbolOrType, _type);
}
exports.provide = provide;
const ProvideInstance = (symbol) => (target) => {
const instance = new target();
exports.__InstanceRegistry.set(symbol, instance);
};
exports.ProvideInstance = ProvideInstance;
function provideInstance(symbolOrConstructor, constructor) {
const _constructor = constructor === undefined ? symbolOrConstructor : constructor;
const instance = new _constructor();
exports.__InstanceRegistry.set(symbolOrConstructor, instance);
return instance;
}
exports.provideInstance = provideInstance;
function provideRawInstance(symbolOrInstance, instance) {
const _symbol = instance === undefined ? symbolOrInstance.constructor : symbolOrInstance;
const _instance = instance === undefined ? symbolOrInstance : instance;
exports.__InstanceRegistry.set(_symbol, _instance);
return _instance;
}
exports.provideRawInstance = provideRawInstance;
const getContext = (instance) => {
return instance[exports.__ContextSymbol];
};
exports.getContext = getContext;
const isContext = (instance) => {
return instance instanceof ContextImpl;
};
exports.isContext = isContext;