npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@lillallol/dic

v2.0.0

Published

My own dependency injection container.

Downloads

10

Readme

DIC

Table of contents

Installation

npm install @lillallol/dic

Description

A dependency injection container (DIC) with the following characteristics:

  • configuration as code (no auto wiring)
    • there will be helpful error messages when a registration has missing or extra dependencies
  • only factory (i.e. functions) registrations
  • singleton and transient lifecycle (no scoped lifecycle)
  • interception at composition
  • ecmascript symbols for interfaces
  • manual injection on object composition
  • state reset for memoized concretions of singleton lifecycle
  • abstraction un-registration

Utility functions are provided that:

  • locate circular loops in the dependency graph
  • find dead registrations and abstractions
  • print the dependency graph

Code coverage

Testing code coverage is around 90%.

Examples

Composition

import { Dic } from "../Dic/Dic";

describe(Dic.name, () => {
    it("creates the concretion of the provided abstraction", () => {
        /**
         * Dependency graph:
         *
         * ```
         *       foo
         *      ↙   ↘
         *    bar   baz
         * ```
         */
        const dic = new Dic();
        const TYPES = {
            foo: Symbol("foo"),
            bar: Symbol("bar"),
            baz: Symbol("baz"),
        };

        type interfaces = {
            foo: (x: number) => number;
            bar: () => number;
            baz: () => number;
        };

        function fooFactory(bar: interfaces["bar"], baz: interfaces["baz"]): interfaces["foo"] {
            return function foo(x) {
                return bar() + baz() + x;
            };
        }
        function barFactory(): interfaces["bar"] {
            return function bar() {
                return 1;
            };
        }
        function bazFactory(): interfaces["baz"] {
            return function baz() {
                return -1;
            };
        }

        dic.register({
            abstraction: TYPES.foo,
            dependencies: [TYPES.bar, TYPES.baz],
            factory: fooFactory,
            lifeCycle: "transient",
        });
        dic.register({
            abstraction: TYPES.bar,
            dependencies: [],
            factory: barFactory,
            lifeCycle: "singleton",
        });
        dic.register({
            abstraction: TYPES.baz,
            dependencies: [],
            factory: bazFactory,
            lifeCycle: "singleton",
        });

        const foo: interfaces["foo"] = dic.get({ abstraction: TYPES.foo });

        expect(foo(0)).toBe(1 + -1 + 0);
    });
});

Manual injection

import { Dic } from "../Dic/Dic";

describe(Dic, () => {
    it("manually injects the provided concretion", () => {
        /**
         * Dependency graph:
         *
         * ```
         *       a
         *      ↙ ↘
         *     b   c
         * ```
         */
        const dic = new Dic();
        const TYPES = {
            a: Symbol("a"),
            b: Symbol("b"),
            c: Symbol("c"),
        };

        type interfaces = {
            a: number;
            b: number;
            c: number;
        };

        function aFactory(b: interfaces["b"], c: interfaces["c"]): interfaces["a"] {
            return b + c;
        }
        function bFactory(): interfaces["b"] {
            return 1;
        }
        function cFactory(): interfaces["c"] {
            return -1;
        }
        dic.register({
            abstraction: TYPES.a,
            dependencies: [TYPES.b, TYPES.c],
            factory: aFactory,
            lifeCycle: "transient",
        });
        dic.register({
            abstraction: TYPES.b,
            dependencies: [],
            factory: bFactory,
            lifeCycle: "singleton",
        });
        dic.register({
            abstraction: TYPES.c,
            dependencies: [],
            factory: cFactory,
            lifeCycle: "singleton",
        });

        const inject = new Map([[TYPES.c, -2]]);

        expect(dic.get({ abstraction: TYPES.a, inject })).toBe(1 + -2);
    });
});

Print dependency graph

import { Dic, printDependencyGraph } from "../";
import { tagUnindent } from "../es-utils/tagUnindent";

describe(printDependencyGraph.name, () => {
    it("prints the dependency graph", () => {
        /**
         * Dependency graph:
         *
         * ```
         *       a
         *      ↙ ↘
         *     b   c
         * ```
         */
        const dic = new Dic();

        const TYPES = {
            a: Symbol("a"),
            b: Symbol("b"),
            c: Symbol("c"),
        };

        type interfaces = {
            a: void;
            b: void;
            c: void;
        };

        function aFactory(b: interfaces["b"], c: interfaces["c"]): interfaces["a"] {
            b; //use b somehow
            c; //use c somehow
            return;
        }

        function bFactory(): interfaces["b"] {
            return;
        }

        function cFactory(): interfaces["c"] {
            return;
        }

        dic.register({
            abstraction: TYPES.a,
            dependencies: [TYPES.b, TYPES.c],
            factory: aFactory,
            lifeCycle: "singleton",
        });

        dic.register({
            abstraction: TYPES.b,
            dependencies: [],
            factory: bFactory,
            lifeCycle: "singleton",
        });

        dic.register({
            abstraction: TYPES.c,
            dependencies: [],
            factory: cFactory,
            lifeCycle: "singleton",
        });

        expect(printDependencyGraph({ TYPES, dic, rootAbstraction: TYPES.a })).toBe(tagUnindent`
            total number of unique components: 3

            a
            |_ b
            |_ c
        `);
    });
});

Dead registrations

import { Dic } from "../Dic/Dic";
import { tagUnindent } from "../es-utils/tagUnindent";
import { validateDependencyGraph } from "../validateDependencyGraph/validateDependencyGraph";

describe(validateDependencyGraph.name, () => {
    it("throws when the combined entry point abstractions not cover the whole dependency graph", () => {
        /**
         * Dependency graph:
         *
         * ```
         *       a   d
         *      ↙ ↘ ↙
         *     b   c
         * ```
         *
         * Entry point abstractions:
         *
         *     a
         *
         * Dead abstraction:
         *
         *     d
         *
         */
        const dic = new Dic();
        const TYPES = {
            a: Symbol("a"),
            b: Symbol("b"),
            c: Symbol("c"),
            d: Symbol("d"),
        };

        type interfaces = {
            a: void;
            b: void;
            c: void;
            d: void;
        };

        function aFactory(b: interfaces["b"], c: interfaces["c"]): interfaces["a"] {
            b; //use b somehow
            c; //use c somehow
            return;
        }
        function bFactory(): interfaces["b"] {
            return;
        }
        function cFactory(): interfaces["c"] {
            return;
        }
        function dFactory(c: interfaces["c"]): interfaces["d"] {
            c; //use c somehow
            return;
        }
        dic.register({
            abstraction: TYPES.a,
            dependencies: [TYPES.b, TYPES.c],
            factory: aFactory,
            lifeCycle: "transient",
        });
        dic.register({
            abstraction: TYPES.b,
            dependencies: [],
            factory: bFactory,
            lifeCycle: "singleton",
        });
        dic.register({
            abstraction: TYPES.c,
            dependencies: [],
            factory: cFactory,
            lifeCycle: "singleton",
        });
        dic.register({
            abstraction: TYPES.d,
            dependencies: [TYPES.c],
            factory: dFactory,
            lifeCycle: "singleton",
        });
        expect(() =>
            validateDependencyGraph({
                TYPES,
                dic,
                entryPointAbstractions: [TYPES.a],
            })
        ).toThrow(tagUnindent`
            The following abstractions:

                Symbol(d)

            are not used by the entry point abstractions:

                Symbol(a)

        `);
    });
});

Graph cycles

import { validateDependencyGraph } from "../";
import { Dic } from "../Dic/Dic";
import { tagUnindent } from "../es-utils/tagUnindent";

describe(validateDependencyGraph.name, () => {
    it("detects circular loops in the dependency graph", () => {
        /**
         * Dependency graph:
         *
         * ```
         *    a  ←  c
         *     ↘   ↗
         *       b
         * ```
         *
         * Entry point abstraction:
         *
         *    a
         *
         */
        const dic = new Dic();
        const TYPES = {
            a: Symbol("a"),
            b: Symbol("b"),
            c: Symbol("c"),
        };
        const entryPointAbstractions = [TYPES.a];
        type interfaces = {
            a: void;
            b: void;
            c: void;
        };
        function aFactory(b: interfaces["b"]): interfaces["a"] {
            b; //use b somehow
        }
        function bFactory(c: interfaces["c"]): interfaces["b"] {
            c; //use c somehow
        }
        function cFactory(a: interfaces["a"]): interfaces["c"] {
            a; //use a somehow
        }
        dic.register({
            abstraction: TYPES.a,
            dependencies: [TYPES.b],
            factory: aFactory,
            lifeCycle: "singleton",
        });
        dic.register({
            abstraction: TYPES.b,
            dependencies: [TYPES.c],
            factory: bFactory,
            lifeCycle: "singleton",
        });
        dic.register({
            abstraction: TYPES.c,
            dependencies: [TYPES.a],
            factory: cFactory,
            lifeCycle: "singleton",
        });

        expect(() =>
            validateDependencyGraph({
                dic,
                entryPointAbstractions,
                TYPES,
            })
        ).toThrow(tagUnindent`
            The composition graph of:
            
                Symbol(a)
            
            has a cycle on the following path:
            
                ┌> Symbol(a)
                │   ↓
                │  Symbol(b)
                │   ↓
                └─ Symbol(c)
        `);
    });
});

Interception

import { Dic } from "../Dic/Dic";

describe(Dic.name, () => {
    it("allows interception", () => {
        const dic = new Dic();
        const TYPES = {
            a: Symbol("a"),
        };
        type interfaces = {
            a: (x1: number, x2: number) => number;
        };
        function aFactory(): interfaces["a"] {
            return function a(x1, x2) {
                return x1 + x2;
            };
        }
        dic.register(
            {
                abstraction: TYPES.a,
                dependencies: [],
                factory: aFactory,
                lifeCycle: "singleton",
            },
            {
                intercept: [
                    ({ concretion }) => {
                        return function a(x1, x2) {
                            if (typeof x1 !== "number") throw Error("`x1` has to be of type number.");
                            if (typeof x2 !== "number") throw Error("`x2` has to be of type number.");
                            return concretion(x1, x2);
                        };
                    },
                ],
            }
        );

        const a: interfaces["a"] = dic.get({ abstraction: TYPES.a });

        //@ts-expect-error
        expect(() => a("0", 1)).toThrow();
    });
});

AOP

You do aspect oriented programming (AOP), when cross cutting concerns (CCC) are applied in a centralized and DRY way:

import { Dic } from "../Dic/Dic";

describe(Dic.name, () => {
    it("enables AOP via interception", () => {
        const dic = new Dic();

        const TYPES = {
            foo: Symbol("foo"),
            bar: Symbol("bar"),
            baz: Symbol("baz"),
        };

        type interfaces = {
            foo: () => void;
            bar: () => void;
            baz: () => void;
        };

        function fooFactory(bar: interfaces["bar"], baz: interfaces["baz"]): interfaces["foo"] {
            return function foo() {
                bar();
                baz();
                return;
            };
        }

        function barFactory(): interfaces["bar"] {
            return function bar() {
                return;
            };
        }

        function bazFactory(): interfaces["baz"] {
            return function baz() {
                return;
            };
        }

        dic.register({
            abstraction: TYPES.foo,
            dependencies: [TYPES.bar, TYPES.baz],
            factory: fooFactory,
            lifeCycle: "transient",
        });

        dic.register({
            abstraction: TYPES.bar,
            dependencies: [],
            factory: barFactory,
            lifeCycle: "singleton",
        });

        dic.register({
            abstraction: TYPES.baz,
            dependencies: [],
            factory: bazFactory,
            lifeCycle: "singleton",
        });

        const callStack: string[] = [];

        dic.registry.forEach((registration) => {
            registration.intercept.push(({ concretion }) => {
                if (typeof concretion === "function") {
                    return (...args: unknown[]) => {
                        callStack.push(concretion.name);
                        return concretion(...args);
                    };
                }
                return concretion;
            });
        });

        const foo: interfaces["foo"] = dic.get({ abstraction: TYPES.foo });

        foo();

        expect(callStack).toEqual(["foo", "bar", "baz"]);
    });
});

Documentation

/**
 * @description
 * Dependency injection container constructor.
 */
export declare const Dic: DicCtor;
export declare type DicCtor = new () => IDic;
export declare type IDic = {
    /**
     * @description
     * Maps abstractions to their corresponding registrations.
     */
    registry: Map<
        symbol,
        {
            abstraction: symbol;
            dependencies: symbol[];
            factory: Function;
            lifeCycle: "singleton" | "transient";
            intercept: ((parameters: { dic: IDic; concretion: any }) => any)[];
        }
    >;
    /**
     * @description
     * All abstractions that have been `get`ed and have singleton lifecycle are
     * memoized in this memoization table.
     */
    memoizationTable: Map<symbol, unknown>;
    /**
     * @description
     * Deletes all the memoized values from the memoization table.
     */
    clearMemoizationTable: () => void;
    /**
     * @description
     * Adds a registration to the dic.
     */
    register: <P extends unknown[], R>(
        arg0: {
            abstraction: symbol;
            dependencies: symbol[];
            factory: (...args: P) => R;
            lifeCycle: "singleton" | "transient";
        },
        arg1?: {
            intercept?: ((parameters: { dic: IDic; concretion: R }) => R)[];
        }
    ) => void;
    /**
     * @description
     * Deletes the registration of the provided abstraction from the registry.
     * It returns `true` if the abstraction registration was found and got
     * deleted, and `false` if it was not found.
     */
    unregister: (parameters: {
        /**
         * @description
         * Abstraction to unregister from the registry.
         */
        abstraction: symbol;
    }) => boolean;
    /**
     * @description
     * Returns the concretion of the provided abstraction.
     */
    get: <T>(parameters: {
        /**
         * @description
         * The abstraction for which you want to get the concretion. Make sure
         * that the symbol is defined with a name (e.g `Symbol("my-name")`) so
         * that more helpful error messages are given.
         */
        abstraction: symbol;
        /**
         * @description
         * Provide manual concretions to be injected when the abstraction
         * dependency graph is composed.
         *
         * The already memoized values override the provided injection values.
         */
        inject?: Map<symbol, unknown>;
    }) => T;
};
/**
 * @description
 * It returns a string representation of the dependency graph starting from the
 * provided abstraction.
 */
export declare const printDependencyGraph: (parameters: {
  dic: IDic;
  rootAbstraction: symbol;
  TYPES: ITYPES;
}) => string;
export declare type IDic = {
    /**
     * @description
     * Maps abstractions to their corresponding registrations.
     */
    registry: Map<
        symbol,
        {
            abstraction: symbol;
            dependencies: symbol[];
            factory: Function;
            lifeCycle: "singleton" | "transient";
            intercept: ((parameters: { dic: IDic; concretion: any }) => any)[];
        }
    >;
    /**
     * @description
     * All abstractions that have been `get`ed and have singleton lifecycle are
     * memoized in this memoization table.
     */
    memoizationTable: Map<symbol, unknown>;
    /**
     * @description
     * Deletes all the memoized values from the memoization table.
     */
    clearMemoizationTable: () => void;
    /**
     * @description
     * Adds a registration to the dic.
     */
    register: <P extends unknown[], R>(
        arg0: {
            abstraction: symbol;
            dependencies: symbol[];
            factory: (...args: P) => R;
            lifeCycle: "singleton" | "transient";
        },
        arg1?: {
            intercept?: ((parameters: { dic: IDic; concretion: R }) => R)[];
        }
    ) => void;
    /**
     * @description
     * Deletes the registration of the provided abstraction from the registry.
     * It returns `true` if the abstraction registration was found and got
     * deleted, and `false` if it was not found.
     */
    unregister: (parameters: {
        /**
         * @description
         * Abstraction to unregister from the registry.
         */
        abstraction: symbol;
    }) => boolean;
    /**
     * @description
     * Returns the concretion of the provided abstraction.
     */
    get: <T>(parameters: {
        /**
         * @description
         * The abstraction for which you want to get the concretion. Make sure
         * that the symbol is defined with a name (e.g `Symbol("my-name")`) so
         * that more helpful error messages are given.
         */
        abstraction: symbol;
        /**
         * @description
         * Provide manual concretions to be injected when the abstraction
         * dependency graph is composed.
         *
         * The already memoized values override the provided injection values.
         */
        inject?: Map<symbol, unknown>;
    }) => T;
};
export declare type ITYPES = {
    [x: string]: symbol;
};
/**
 * @description
 * Provide `TYPES` to get back an identity function that provides intellisense
 * for the keys of `TYPES`. This function can be used to have refactor-able
 * names in the specification of unit tests.
 */
export declare const namesFactory: <T extends ITYPES>() => <
  N extends keyof T
>(
  name: N
) => N;
export declare type ITYPES = {
    [x: string]: symbol;
};
/**
 * @description
 * It throws error when:
 *
 * * the dependency graph of the provided entry abstractions
 *   does not use all the registered abstractions
 * * `TYPES` has extra or missing abstractions
 * * there are cycles in the dependency graph
 *
 */
export declare const validateDependencyGraph: (parameters: {
  dic: IDic;
  entryPointAbstractions: symbol[];
  TYPES: ITYPES;
  ignoreAbstractions?: symbol[] | undefined;
}) => void;
export declare type IDic = {
    /**
     * @description
     * Maps abstractions to their corresponding registrations.
     */
    registry: Map<
        symbol,
        {
            abstraction: symbol;
            dependencies: symbol[];
            factory: Function;
            lifeCycle: "singleton" | "transient";
            intercept: ((parameters: { dic: IDic; concretion: any }) => any)[];
        }
    >;
    /**
     * @description
     * All abstractions that have been `get`ed and have singleton lifecycle are
     * memoized in this memoization table.
     */
    memoizationTable: Map<symbol, unknown>;
    /**
     * @description
     * Deletes all the memoized values from the memoization table.
     */
    clearMemoizationTable: () => void;
    /**
     * @description
     * Adds a registration to the dic.
     */
    register: <P extends unknown[], R>(
        arg0: {
            abstraction: symbol;
            dependencies: symbol[];
            factory: (...args: P) => R;
            lifeCycle: "singleton" | "transient";
        },
        arg1?: {
            intercept?: ((parameters: { dic: IDic; concretion: R }) => R)[];
        }
    ) => void;
    /**
     * @description
     * Deletes the registration of the provided abstraction from the registry.
     * It returns `true` if the abstraction registration was found and got
     * deleted, and `false` if it was not found.
     */
    unregister: (parameters: {
        /**
         * @description
         * Abstraction to unregister from the registry.
         */
        abstraction: symbol;
    }) => boolean;
    /**
     * @description
     * Returns the concretion of the provided abstraction.
     */
    get: <T>(parameters: {
        /**
         * @description
         * The abstraction for which you want to get the concretion. Make sure
         * that the symbol is defined with a name (e.g `Symbol("my-name")`) so
         * that more helpful error messages are given.
         */
        abstraction: symbol;
        /**
         * @description
         * Provide manual concretions to be injected when the abstraction
         * dependency graph is composed.
         *
         * The already memoized values override the provided injection values.
         */
        inject?: Map<symbol, unknown>;
    }) => T;
};
export declare type ITYPES = {
    [x: string]: symbol;
};

Motivation

Made for learning purposes but ended up using it in my own projects, so I decided to publish it to npm.

Acknowledgments

The following resources had a detrimental role in the creation of this module:

Contributing

I am open to suggestions/pull request to improve this program.

You will find the following commands useful:

  • Clones the github repository of this project:

    git clone https://github.com/lillallol/dic
  • Installs the node modules (nothing will work without them):

    npm install
  • Tests the source code:

    npm run test
  • Lints the source folder using typescript and eslint:

    npm run lint
  • Builds the typescript code from the ./src folder to javascript code in ./dist:

    npm run build-ts
  • Injects in place the generated toc and imported files to README.md:

    npm run build-md
  • Checks the project for spelling mistakes:

    npm run spell-check

    Take a look at the related configuration ./cspell.json.

  • Checks ./src for dead typescript files:

    npm run dead-files

    Take a look at the related configuration ./unimportedrc.json.

  • Logs in terminal which dependencies and devDependencies have a new version published in npm:

    npm run check-updates
  • Updates the dependencies and devDependencies to their latest version:

    npm run update
  • Formats all .ts files of the ./src folder:

    npm run format

Changelog

2.0.0

breaking changes

  • Symbols that are used for abstractions have to be defined with a name. For example:

    const TYPES = {
        myAbstraction: Symbol("myAbstraction"),
    };

    This is done to have more helpful error messages.

  • The intercept argument of dic.get is now on its own object in a second optional argument. This was done to avoid limitations in type inference:

    Old:

    No linting errors for trivial interception:

    dic.register(
        {
            abstraction: Symbol("A"),
            dependencies: [],
            factory: function A(): () => number {
                return (): number => 1;
            },
            lifeCycle: "singleton",
        },
        {
            intercept: [
                ({ concretion }) => {
                    return concretion;
                },
            ],
        }
    );

    Lints error for non trivial interception:

    dic.register({
        abstraction: Symbol("A"),
        dependencies: [],
        factory: function A(): () => number {
            return (): number => 1;
        },
        lifeCycle: "singleton",
        intercept: [
            ({ concretion }) => {
                // lints error here
                return () => concretion();
            },
        ],
    });

    New:

    No linting errors for non trivial interception:

    dic.get(
        {
            abstraction: Symbol("A"),
            dependencies: [],
            factory: function A(): () => number {
                return (): number => 1;
            },
            lifeCycle: "singleton",
        },
        {
            intercept: [
                ({ concretion }) => {
                    return () => concretion();
                },
            ],
        }
    );

    notice that get now receives two parameters instead of single one.

    Acknowledgements.

  • throwIfDeadRegistrations has been renamed to validateDependencyGraph. It now has TYPES as required parameter.That is because it finds extra or missing abstractions of TYPES object. It also detects circular loops in the dependency graph. Finally you can specify those abstractions that are correctly not used by your entry point abstractions via the parameter ignoreAbstractions.

  • printDependencyTree has been renamed to printDependencyGraph.

  • Factories that are registered have to have a name property that is of non zero length and not equal to string "factory". This is done to have more helpful error messages.

  • The properties _memoizationTable and _registry of Dic instances have been renamed to memoizationTable and registry respectively.

  • Registrations no longer have property hasBeenMemoized.

Other

  • Added sections Contributing, Changelog, Code coverage, in README.md.
  • Added actual documentation in the Documentation section of README.md.

1.1.0

non breaking changes

  • Added function throwIfDeadRegistrations which throws error when there are dead registrations in the dic.

other

  • Added CHANGELOG.md.

1.0.0

  • Published the package.

License

MIT