/** A generic constructor */ type Constructor = abstract new (...args: any) => T /** The _type_ for a binding, either a `Constructor` or a `string`. */ type Binding = Constructor | string /** The _type_ of a binding in the context of an `Injector`. */ type InjectorBinding< Components extends Constructor, Provisions extends Record, > = Components | (keyof Provisions & string) | PromisedBinding /** A tuple of `InjectorBinding`s (what's needed by `$inject`). */ type InjectTuple< Components extends Constructor, Provisions extends Record, > = readonly [ InjectorBinding, ...(InjectorBinding)[] ] /* ========================================================================== */ /** * Check that an `Injector`'s static `$inject` member matches the `Injector`'s * own components and provisions */ type CheckInject< Inject, // the tuple from "$inject" Components extends Constructor, // the `Injector`'s components Provisions extends Record, // the `Injector`'s provisions > = // if "$inject" is an empty array, then we're fine Inject extends readonly [] ? readonly [] : // if "$inject" is a single-element tuple, check that that this element is // actually included in the list of components Inject extends readonly [ infer I1 ] ? I1 extends PromisedBinding ? I2 extends keyof Provisions ? readonly [ PromisedBinding ] : I2 extends Extract ? readonly [ PromisedBinding ] : readonly [ PromisedBinding ] : I1 extends keyof Provisions ? readonly [ I1 ] : I1 extends Extract ? readonly [ I1 ] : readonly [ never ] : // if "$inject" is a multi-element tuple, recurse (twice) by cheking the first // element as a single-element tuple (see above), and the remaining members. Inject extends readonly [ infer I1, ...infer I2 ] ? readonly [ ...CheckInject<[ I1 ], Components, Provisions>, ...CheckInject, ] : // "$inject" here is something else (likely, an array but not "as const") // so we want to fail by re-declaring it as a tuple readonly [ Components | keyof Provisions, ...(Components | keyof Provisions)[] ] /* ========================================================================== */ /** * Map the contents of the static `$inject` member to their types, resolving * named provisions. */ type MapInject< Inject, // the tuple from "$inject" Provisions extends Record, // the `Injector`'s provisions > = // if "$inject" is an empty array, then we're fine Inject extends readonly [] ? readonly [] : // "$inject" is a single-element tuple Inject extends readonly [ infer I1 ] ? I1 extends PromisedBinding ? // promised bindings I2 extends keyof Provisions ? readonly [ Promise ] : I2 extends Constructor ? readonly [ Promise> ] : readonly [ never ] : // resolved (non promised) bindings I1 extends keyof Provisions ? readonly [ Provisions[I1] ] : I1 extends Constructor ? readonly [ InstanceType ] : readonly [ never ] : // "$inject" is a multi-element tuple (recurse) Inject extends readonly [ infer I1, ...infer I2 ] ? readonly [ ...MapInject<[ I1 ], Provisions>, ...MapInject, ] : // "$inject" is something else readonly [ never ] /* ========================================================================== */ /** * Check that an `Injectable` is valid for a given `Injector`. */ type CheckInjectable< I extends Injectable, // the `Injectable` to check Components extends Constructor, // the `Injector`'s components Provisions extends Record, // the `Injector`'s provisions > = // If the static "$inject" member is specified, check it and map its // types as constructor arguments for the `Injectable` I extends { $inject: infer Inject } ? { $inject: CheckInject, new (...args: MapInject): any, } : // If "$inject" is not specified, the only valid injector is one with // an empty (zero arguments) constructor I extends new () => any ? I : // Anything else requires "$inject" to be present { $inject: InjectTuple } /* ========================================================================== */ /** * Return the unrolled type of a `Promise` */ type UnrollPromise = T extends Promise ? UnrollPromise : T /* ========================================================================== */ /** * Override the type for an `Injector`'s existing provision, or add a new one */ type ExtendProvisions< Provisions extends Record, // the `Injector`'s provisions P extends string, // the new (string) key of the provision T, // the new type associated with the provision key > = { [ key in P | keyof Provisions ]: key extends P ? UnrollPromise : Provisions[key] } /* ========================================================================== * * EXPORTED TYPES * * ========================================================================== */ /** Utility to nicely print a binding name */ function bindingName(binding: Binding): string { return typeof binding === 'function' ? `[class ${binding.name}]` : `"${binding}"` } /** A constant symbol identifying a _promised binding_. */ const promisedBinding = Symbol.for('siringa.promisedBinding') /** Declare a _binding_ to be _promised_ (inject its `Promise`). */ export function promise(binding: B): PromisedBinding { return { [promisedBinding]: binding } } /* ========================================================================== */ export interface PromisedBinding { [promisedBinding]: B } /** An `Injectable` defines a constructor for an injectable class */ export interface Injectable< Components extends Constructor, Provisions extends Record, T = any, > { prototype: T new (...args: any): T $inject?: InjectTuple } /** * The `Injections` interface abstracts the idea of getting bound and * provisioned instances from an `Injector`, injecting new `Injectable` * instances, and creating sub-`Injector`s. */ export interface Injections< Components extends Constructor = never, Provisions extends Record = {}, > { /** Get a _bound_ instance from an `Injector`. */ get(component: C): Promise> /** Get a _provisioned_ instance from an `Injector`. */ get

(provision: P): Promise /** * Create a new instance of the specified `Injectable`, providing it with all * necessary injections. * * @param injectable The constructor of the instance to create. */ inject>( injectable: I & CheckInjectable, ): Promise> /** Create a sub-`Injector` child of the current one. */ injector(): Injector } /** A `Factory` is a _function_ creating instances of a given type. */ export type Factory< Components extends Constructor = Constructor, Provisions extends Record = Record, T = any, > = (injections: Injections) => T | Promise /** * The `Injector` class acts as a registry of components and provisions, * creating instances and injecting dependencies. */ export class Injector< Components extends Constructor = never, Provisions extends Record = {}, > implements Injections { readonly #factories: Map Promise> = new Map() readonly #promises: Map> = new Map() #parent?: Injector> /* INTERNALS ============================================================== */ async #get(binding: Binding, stack: Binding[]): Promise { if (stack.includes(binding)) { if (this.#parent) return this.#parent.#get(binding, []) const message = `Recursion detected injecting ${bindingName(binding)}` return Promise.reject(new Error(message)) } const promise = this.#promises.get(binding) if (promise) return promise const factory = this.#factories.get(binding) if (factory) { const promise = Promise.resolve().then(() => factory([ ...stack, binding ])) this.#promises.set(binding, promise) return promise } if (this.#parent) return this.#parent.#get(binding, []) const message = `Unable to resolve binding ${bindingName(binding)}` return Promise.reject(new Error(message)) } async #inject(injectable: Injectable, stack: Binding[]): Promise { const promises = injectable.$inject?.map((binding: Binding | PromisedBinding) => { switch (typeof binding) { case 'string': case 'function': return this.#get(binding, stack) default: return binding } }) const injections = promises ? (await Promise.all(promises)).map((i) => { return i && (typeof i === 'object') && (i[promisedBinding]) ? this.#get(i[promisedBinding], stack) : i }) : [] // eslint-disable-next-line new-cap return new injectable(...injections) } /* BINDING ================================================================ */ /** Bind an `Injectable` to this `Injector`. */ bind>( injectable: I & CheckInjectable, ): Injector /** Bind an `Injectable` to a `Constructor` in this `Injector`. */ bind>>( component: C, injectable: I & CheckInjectable, ): Injector /** Bind an `Injectable` to a name in this `Injector`. */ bind

>( provision: P, injectable: I & CheckInjectable, ): Injector>> // Overloaded implementation bind( binding: Binding, maybeInjectable?: Injectable, ): this { const injectable = maybeInjectable ? maybeInjectable : binding as Injectable this.#factories.set(binding, async (stack) => this.#inject(injectable, stack)) return this } /* FACTORIES ============================================================== */ /** Use a `Factory` to create instances bound to the given `Constructor`. */ create( component: C, factory: Factory>, ): Injector /** Use a `Factory` to create instances bound to the given name. */ create

>( provision: P, factory: F, ): Injector>> // Overloaded implementation create( binding: Binding, factory: Factory, ): this { this.#factories.set(binding, async (stack) => factory({ get: (component: any) => this.#get(component, stack), inject: async (injectable: any) => this.#inject(injectable, stack), injector: () => this.injector(), })) return this } /* ENVIRONMENT VARIABLES ================================================== */ /** Provision an environment variable. */ env( variable: E, defaultValue?: string, ): Injector> { const value = globalThis.process?.env?.[variable] || defaultValue if (! value) { throw new Error(`Environment variable "${variable}" is not defined`) } else { this.#promises.set(variable, Promise.resolve(value)) } return this } /* INSTANCES ============================================================== */ /** Use the given instance binding it to to the given `Constructor`. */ use( component: C, instance: InstanceType | PromiseLike>, ): Injector /** Use the given instance binding it to to the given name. */ use

( provision: P, instance: T | PromiseLike, ): Injector> // Overloaded implementation use( binding: Binding, instance: any, ): this { this.#promises.set(binding, Promise.resolve(instance)) return this } /* INSTANCES ============================================================== */ /** Get a _bound_ instance from an `Injector` */ get(component: C): Promise> /** Get a _provisioned_ instance from an `Injector` */ get

(provision: P): Promise // Overloaded implementation async get( binding: B, ): Promise { return this.#get(binding, []) } /* INJECTIONS ============================================================= */ /** * Create a new instance of the specified `Injectable`, providing all * necessary injections. * * @param injectable The constructor of the instance to create. */ inject>( injectable: I & CheckInjectable, ): Promise> { return this.#inject(injectable, []) } /** * Simple utility method to invoke the factory with the correct `Injections` * and return its result. * * This can be used to alleviate issues when top-level await is not available. */ make>( factory: F, ): ReturnType { return factory({ get: (component: any) => this.#get(component, []), inject: async (injectable: any) => this.#inject(injectable, []), injector: () => this.injector(), }) } /* CHILD INJECTORS ======================================================== */ /** Create a sub-`Injector` child of this one. */ injector(): Injector { const injector = new Injector() injector.#parent = this return injector } }