containor
Version:
Simple DI container for Javascript with Typescript support
307 lines (295 loc) • 11.4 kB
JavaScript
var Containor = (function (exports) {
'use strict';
const indices = ["First", "Second", "Third"];
function validateArguments(method, options) {
options.map(([value, expected, required], index) => {
const expectedName = typeof expected === "string" ? expected : expected.name;
const matches = typeMatches(expected, value);
return invariant(required ? matches : value === undefined || value === null || matches, `${indices[index]} argument of "${method}" should be of type: "${expectedName}", received: "${typeof value}".`);
});
}
function typeMatches(expected, value) {
if (typeof expected === "string") {
const type = Array.isArray(value) ? "array" : typeof value;
return type === expected;
}
return value instanceof expected;
}
function invariant(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function raw(value) {
return new RawArgument(value);
}
class RawArgument {
constructor(value) {
this.value = value;
}
}
function token(name) {
return new Token(name);
}
class Token {
constructor(name) {
this.type = null;
this.name = name;
}
}
class Providers {
constructor() {
this.providers = [];
this.reservations = new Set();
}
add(tokens, callback, called = false, reserved = false) {
tokens.forEach((token) => {
invariant(token instanceof Token, `Trying to provide non token argument.`);
invariant(reserved || !this.reservations.has(token.name), `Provider for "${token.name}" is already reserved.`);
invariant(!this.has(token), `Dependency "${token.name}" is already being provided.`);
});
this.providers.push({ tokens, callback, called });
tokens.forEach((token) => {
if (reserved) {
this.cancelReservation(token);
}
});
}
has(token) {
return Boolean(this.get(token));
}
notify(token) {
const provider = this.get(token);
if (provider) {
provider.callback();
provider.called = true;
}
}
get(token) {
return this.providers.find((provider) => !provider.called &&
provider.tokens.some((providerToken) => providerToken.name === token.name));
}
reserve(token) {
invariant(!this.reservations.has(token.name), `Provider for "${token.name}" is already reserved.`);
this.reservations.add(token.name);
}
cancelReservation(token) {
this.reservations.delete(token.name);
}
isReserved(token) {
return this.reservations.has(token.name);
}
}
class Requests {
constructor() {
this.requests = [];
}
add(token, callback) {
this.requests.push({ token, callback });
}
fulfill(token) {
this.requests
.filter((request) => request.token.name === token.name)
.forEach((request) => request.callback());
this.remove(token);
}
has(token) {
return this.requests.some((request) => request.token.name === token.name);
}
remove(token) {
this.requests = this.requests.filter((request) => request.token.name !== token.name);
}
}
class Dependencies {
constructor() {
this.dependencies = new Map();
this.reservations = new Set();
}
add(token, creator, args, shared = false, reserved = false) {
invariant(!this.dependencies.get(token.name), `Dependency "${token.name}" already exists.`);
invariant(reserved || !this.reservations.has(token.name), `Dependency "${token.name}" has already been reserved.`);
const argsArr = Array.isArray(args) ? args : [];
invariant(!argsArr.some((arg) => arg instanceof Token && arg.name === token.name), `Trying to add a recursive dependency "${token.name}".`);
this.dependencies.set(token.name, {
token,
creator,
args: argsArr,
shared,
});
if (reserved) {
this.cancelReservation(token);
}
}
get(token) {
invariant(this.has(token), `Dependency "${token.name}" does not exist.`);
return this.dependencies.get(token.name);
}
has(token) {
return this.dependencies.has(token.name);
}
reserve(token) {
invariant(!this.reservations.has(token.name), `Dependency "${token.name}" is already reserved.`);
this.reservations.add(token.name);
}
cancelReservation(token) {
this.reservations.delete(token.name);
}
isReserved(token) {
return this.reservations.has(token.name);
}
}
class Container {
constructor() {
this.providers = new Providers();
this.requests = new Requests();
this.dependencies = new Dependencies();
}
add(token, creator, args) {
this.register(token, creator, args, "add");
}
addAsync(token, asyncCreator, args) {
return this.registerAsync(token, asyncCreator, args, "add", true);
}
share(token, creator, args) {
this.register(token, creator, args, "share", true);
}
shareAsync(token, asyncCreator, args) {
return this.registerAsync(token, asyncCreator, args, "share", true);
}
constant(token, value) {
this.register(token, () => value, [], "constant", true);
}
constantAsync(token, asyncValue) {
return this.registerAsync(token, asyncValue.then((value) => () => value), [], "constant", true);
}
register(token, creator, args, method, shared = false, reserved = false) {
validateArguments(method, [
[token, Token, true],
[creator, "function", true],
[args, "array"],
]);
this.dependencies.add(token, creator, args, shared, reserved);
this.requests.fulfill(token);
}
registerAsync(token, asyncCreator, args, method, shared = false) {
this.dependencies.reserve(token);
return asyncCreator
.then((creator) => {
this.register(token, creator, args, method, shared, true);
})
.catch((error) => {
this.dependencies.cancelReservation(token);
throw error;
});
}
get(token) {
validateArguments("get", [[token, Token, true]]);
if (this.providers.has(token)) {
this.providers.notify(token);
}
return this.instantiate(this.dependencies.get(token));
}
getAsync(token) {
validateArguments("getAsync", [[token, Token, true]]);
if (this.providers.has(token)) {
this.providers.notify(token);
}
if (this.dependencies.has(token)) {
return Promise.resolve(this.instantiate(this.dependencies.get(token)));
}
return new Promise((resolve) => {
this.requests.add(token, () => {
this.instantiateAsync(this.dependencies.get(token), resolve);
});
});
}
provide(tokens, provider) {
validateArguments("provide", [[tokens, "array", true]]);
const existing = tokens.find((token) => this.dependencies.has(token) || this.dependencies.isReserved(token));
invariant(!existing, `Trying to provide dependency "${existing && existing.name}" which ${existing && this.dependencies.isReserved(existing)
? "is already reserved"
: "already exists"}.`);
if (tokens.some((token) => this.requests.has(token))) {
this.providers.add(tokens, () => { }, true);
provider(this);
}
this.providers.add(tokens, () => provider(this));
}
provideAsync(tokens, asyncProvider) {
asyncProvider.then((provider) => {
this.provide(tokens, provider);
});
}
use(module) {
const moduleInstance = this.construct(module, []);
this.provide(moduleInstance.provides, moduleInstance.register.bind(moduleInstance));
}
useAsync(asyncModule) {
asyncModule.then((module) => {
this.use(module);
});
}
instantiate(dependency) {
if (dependency.shared) {
return dependency.instance || this.createSingle(dependency);
}
return this.create(dependency);
}
instantiateAsync(dependency, callback) {
if (dependency.shared) {
return dependency.instance
? callback(dependency.instance)
: this.createSingleAsync(dependency, callback);
}
return this.createAsync(dependency, callback);
}
create(dependency) {
return this.construct(dependency.creator, this.resolve(dependency.args));
}
createAsync(dependency, callback) {
this.resolveAsync(dependency.args, (resolvedArgs) => callback(this.construct(dependency.creator, resolvedArgs)));
}
createSingle(dependency) {
dependency.instance = this.create(dependency);
return dependency.instance;
}
createSingleAsync(dependency, callback) {
this.createAsync(dependency, (newInstance) => {
dependency.instance = newInstance;
callback(newInstance);
});
}
resolve(args) {
return args.map((arg) => arg instanceof RawArgument ? arg.value : this.get(arg));
}
resolveAsync(args, callback) {
args.reduceRight((next, arg) => (resolvedArgs) => {
return arg instanceof RawArgument
? next([...resolvedArgs, arg.value])
: this.getAsync(arg).then((dependency) => next([...resolvedArgs, dependency]));
}, callback)([]);
}
construct(creator, args) {
return creator.hasOwnProperty("prototype")
? new creator(...args)
: creator(...args);
}
}
function createContainer() {
return new Container();
}
function createModule(provides, register) {
return () => ({
provides,
register,
});
}
exports.Container = Container;
exports.RawArgument = RawArgument;
exports.Token = Token;
exports.createContainer = createContainer;
exports.createModule = createModule;
exports.raw = raw;
exports.token = token;
return exports;
})({});