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

scodi

v1.0.0

Published

Scoped depenency injection container

Downloads

5

Readme

Scodi - Scoped Dependency Injection Container

A Dependency Injection Container/Service Locator for JavaScript.

Note: When reading this, it's assumed you're familiar with IoC and DI.

Scodi provides easy dependency management with 3 basic concepts - services, parameters and scopes.

Basic usage

const { buildContainer } = require('scodi');

const getHelloPhrase = () => 'Hello';

const config = {
  services: {
    getHelloPhrase: {
      // A factory must always wrap the returned service in a function, even when there are no dependencies
      factory: (/* no dependencies */) => getHelloPhrase,
    },
    sayHi: {
      factory: (getPhrase, what) => () => `${getPhrase()} ${what}!`,
      // The specified dependencies are injected to the factory in the order they're defined here
      dependencies: ['@getHelloPhrase', '%whatToSayHiTo'],
    },
  },

  parameters: {
    whatToSayHiTo: 'world',
  },

  scopeTypes: {},
}

const container = buildContainer(config);

const sayHi = container.get('@sayHi');

sayHi() // "Hello world!"

Services

A service is a function that can have dependencies. A dependency can be another service, a parameter, or a scope value (if the service is in a scope that has that value). Services are created with factory functions, which get dependencies as parameters. Services always belong to a scope (however the default is the special scope called SCOPE_GLOBAL - meaning it's accessible from anywhere). A service can only depend on services or scope values that are accessible from the current scope. Services are prefixed with @. They are defined in the services prop in the config.

Configuration

  • factory - Function that instantiates the service, meaning it should take dependencies as parameters and return the service function. Required.
  • dependencies - Array|Object that takes the dependencies that'll be passed to the factory. Use array to get dependencies as anonymous parameters, and object to get named dependencies. Optional, defaults to empty array.
  • scopes - Array[String] of scope types the service belongs to. Optional, defaults to [SCOPE_GLOBAL] (meaning it's accessible from all scopes).
  • type - String - should be TYPE_SINGLETON or TYPE_EVERY_INSTANCE. Defines how the service should be instantiated. TYPE_SINGLETON will return the same instance every time the service is requested, and TYPE_EVERY_INSTANCE will return a new instance of the service every time. Optional, defaults to TYPE_SINGLETON.

The default service configuration:

{
  factory: () => {}, // REQUIRED
  dependencies: [],
  type: TYPE_SINGLETON,
  scopes: [SCOPE_GLOBAL],
}

Example:

const { buildContainer } = require('scodi');

const createBar = baz => () => {
  console.log('bar says: ', baz);
};

const createFoo = bar => (something) => {
  bar();

  console.log('foo says: ', something);
};

const config = {
  services: {
    foo: {
      factory: createFoo,
      dependencies: ['@bar'],
    },

    bar: {
      factory: createBar,
      dependencies: ['%baz'],
    },
  },

  parameters: {
    baz: 'whatever',
  },

  scopeTypes: {}, // See section Scopes to learn what this is
}

const container = buildContainer(config);

const foo = container.get('@foo');
const bar = container.get('@bar');

foo('1') // "bar says: whatever, foo says: 1"
foo('1337') // "bar says: whatever, foo says: 1337"
bar() // "bar says: whatever"

Anonymous vs named dependencies

Dependencies can be defined in two ways; anonymous and named. With anonymous dependencies, you simply pass an array of dependencies, which you will later receive as function parameters in your factory. This can, however, become inconvenient if your service has many dependencies (since you have to keep track of which order you pass them in). If this is the case you can use named dependencies like so:

{
  services: {
    // Anonymous dependencies
    serviceWithAnonymousDependencies: {
      factory: (foo, bar, baz) => () => { foo(); bar(); baz(); },
      dependencies: ['@foo', '@bar', '@baz'],
    },

    // Named dependencies
    serviceWithNamedDependencies: {
      factory: (deps) => () => { deps.foo(); deps.bar(); deps.baz(); },
      dependencies: {
        foo: '@foo',
        bar: '@bar',
        baz: '@baz',
      },
    },

    // Decompose!
    serviceWithDepcomposedNamedDependencies: {
      factory: ({ foo, bar, baz }) => () => { foo(); bar(); baz(); },
      dependencies: {
        foo: '@foo',
        bar: '@bar',
        baz: '@baz',
      },
    },

    // ...
  },
}

Singleton vs every instance

const { buildContainer, TYPE_EVERY_INSTANCE } = require('scodi');

const config = {
  services: {
    singleton: {
      factory: (random) => {
        const randomValue = random(); // Save value in factory

        return () => randomValue;
      },
      dependencies: ['@random'],
      // No need to specify `type: TYPE_SINGLETON`, since it's the default
    },
    everyInstance: {
      factory: (random) => {
        const randomValue = random(); // Save value in factory

        return () => randomValue;
      },
      dependencies: ['@random'],
      type: TYPE_EVERY_INSTANCE,
    },
    random: {
      factory: () => Math.random,
    },
  },

  parameters: {},
  scopeTypes: {},
};

const container = buildContainer(config)

container.get('@singleton')(); // i.e. 0.33941396275422875
container.get('@singleton')(); // same value (0.33941396275422875)

container.get('@everyInstance')(); // i.e. 0.808203227270367
container.get('@everyInstance')(); // another value, i.e. 0.7816774947232212

Parameters

A parameter is value that does not have any dependencies (so basically a key-value object). It should mainly be used for configuration etc. Parameters are always in the global scope. Parameters are prefixed with %.

Example:

const { buildContainer } = require('scodi');

const config = {
  services: {
    foo: {
      factory: bar => () => bar,
      dependencies: ['%bar'],
    },
  },

  parameters: {
    bar: 'whatever',
  },

  scopeTypes: {},
}

const container = buildContainer(config);

container.get('%bar') // "whatever"
container.get('@foo')() // "whatever"

Scopes

A scope is basically new instance of the container, created during runtime, that has scope values. Scope values are like parameters, but are passed when creating the scope (during runtime), meaning they are very useful for i.e. passing the current request's user or locale as a dependency instead of a regular function parameter to the services. Scope values are prefixed with #, and scope types are defined in the scopesTypes props in the config. A scope type simply defines which scope value must be passed when creating the scope.

A service within a subscope can depend on a service from the global scope, or the same scope, but a service from the global scope can not depend on a service from a subscope. In the below example the service "@bar" is dependent on "@foo", but "@foo" could not be dependent on "@bar". Also, a service in the global scope can not be dependent on a scope value, meaning "@foo" can not depend on "#baz".

Example usage:

const { buildContainer } = require('scodi');

const config = {
  services: {
    foo: {
      factory: () => () => 'im foo!',
    },
    bar: {
      factory: (foo, baz) => () => `${foo()} - bar says: ${baz}`,
      dependencies: ['@foo', '#baz'],
      scopes: ['someScope'], // Specify it should be in the "someScope" scope
    },
  },

  parameters: {},

  scopeTypes: {
    // Define that the scope type "someScope" must be created with the value "baz"
    someScope: ['baz'],
  },
};

const container = buildContainer(config);

// Normal container .get
const foo = container.get('@foo');
foo(); // "im foo!"

container.get('@bar'); // Error thrown! 'Service "@bar" is not in the global scope'

container.createScope('someScope') // Error thrown! 'No scope values provided when creating scope someScope'
container.createScope('someScope', { notBaz: '' }) // Error thrown! 'Missing scope value "#baz"'

const scope1container = container.createScope('someScope', { baz: 'a value' });
const scope2container = container.createScope('someScope', { baz: 'another value' });

scope1container.get('@foo')() // "im foo!", returns the same instance as from the global scope, meaning container.get('@foo') === scope1container.get('@foo')
scope1container.get('@bar')() // "im foo! - bar says: a value"
scope2container.get('@bar')() // "im foo! - bar says: another value"