UNPKG

containor

Version:

Simple DI container for Javascript with Typescript support

307 lines (295 loc) 11.4 kB
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; })({});