SkillAgentSearch skills...

Ashley

Ashley is a dependency injection container for JavaScript.

Install / Use

/learn @jiripospisil/Ashley
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Ashley

Ashley is a dependency injection container for JavaScript. Learn more about dependency injection or more generally about inversion of control on Wikipedia.

Installation

npm install ashley

Note that it makes a heavy use of async functions and thus requires a fairly recent version of Node.js (7.x or newer). Depending on the version, you might need to pass the --harmony-async-await flag.

Usage

A new instance of Ashley can be created simply by calling its constructor. Ashley instances do not share any state and there can be any number of them within the same application. If fact, it is sometimes beneficial to have more than one them as they can form hierarchies. More on that later.

const Ashley = require('ashley');
const ashley = new Ashley();

Note that the code samples will use the container directly for obtaining the configured objects. To take advantage of the dependency injection pattern in a real application, the container should be only used explicitly during the application's initialization process to set up the dependencies. Read more in the Recommendations section.

Binding instances

The most basic thing Ashley can bind is an instance of a class. A class in this context is anything that needs to be instantiated with the new operator.

ashley.instance('Logger', ConsoleLogger);
const logger = await ashley.resolve('Logger')

// the same as
const logger = new ConsoleLogger();

The first argument of the instance method is a name. This name can be used when resolving instances or declaring dependencies. The second argument can either be the class itself or a path to a file which defines it. Finally, the third argument is a list of dependencies.

ashley.instance('Logger', ConsoleLogger);
ashley.instance('OrderService', OrderService, ['Logger']);
const orderService = await ashley.resolve('OrderService')

// the same as
ashley.instance('Logger', 'src/console_logger');
ashley.instance('OrderService', 'src/order_service', ['Logger']);
const orderService = await ashley.resolve('OrderService')

// the same as
const logger = new ConsoleLogger();
const orderService = new Orderservice(logger);

Note that when a relative path is provided, Ashley needs to know the root path from which the relative paths should be resolved.

const ashley = new Ashley({
  root: __dirname
});

There are objects within all applications which are meant to be used as singletons but making them actual singletons is problematic.

An alternative is to write regular classes but let Ashley worry about their life time (scope) once they are instantiated. Ashley provides two scopes out of the box - Singleton and Prototype. The Singleton scope is used by default and will make Ashley to always return the same instance each time it's requested. The Prototype scope, on the other hand, will make Ashley to always create new instances when requested.

ashley.instance('DbConnection', 'src/rethink_db_connection', [], {
  scope: 'Singleton' // default
});

ashley.instance('TimePoint', 'src/time_point', [], {
  scope: 'Prototype'
});

For an object such as DbConnection to be useful, it needs to actually establish an connection which will most likely be an asynchronous process. When binding the object, it's possible to specify that an initialization method needs to be called for the object to be fully ready. This is similar to a constructor but allows the method to be asynchronous.

ashley.instance('DbConnection', 'src/rethink_db_connection', [], {
  initialize: true
});

When set to true, Ashley will look for an async method called initialize and will wait for it to finish before proceeding. It's possible to specify a different initialize method by setting initialize to the name.

class RethinkDbConnection {
  async init() {
    this.connection = await r.connect();
  }
}

ashley.instance('DbConnection', RethinkDbConnection, [], {
  initialize: 'init'
});

The initialize method should either succeed or throw an error. It's important to make sure that time outs are set and handled as well otherwise Ashley might wait indefinitely.

The initialization method is the same for all instances of the object. Ashley also provides a way to set up a specific instance by defining an setup function when binding the instance.

ashley.instance('ErrorLogger', ConsoleLogger, [], {
  setup: function(logger) {
    logger.setBold(true);
    logger.setColor(ConsoleLogger.COLOR_RED);
  }
});

ashley.instance('Logger', ConsoleLogger);

The setup function will receive the instantiated object as its only parameter. Note that the function will be called only once if the scope is set to Singleton and may or may not be an async function.

There's also the option to deinitialize instances which works the same way except it's invoked when the container is being shut down. It generally depends on the scope used whether the method is supported or when it's called.

The provided Singleton and Prototype scopes will call the method only when the container is being shutdown. This means that the individual instances need to be kept in memory until that happens. Consider calling the deinitialize method manually on the objects in case they are short lived and keeping them in memory until the container shuts down is problematic.

class RethinkDbConnection {
  async initialize() {
    this.connection = await r.connect();
  }

  async deinitialize() {
    if (this.connection) {
      await this.connection.close();
    }
  }
}

ashley.instance('DbConnection', RethinkDbConnection, [], {
  initialize: true,
  deinitialize: true
});

// ...

await ashley.shutdown();

Note that Ashley does NOT catch errors from these methods. It's up to the developer to handle the failure scenarios themselves. It's especially important for the deinitialize method as throwing within the method will halt de-initialization of the remaining binds.

Binding plain objects

Not everything needs to be wrapped in a class and sometimes it's convenient to bind just plain objects.

ashley.object('Config', { port: 9001 });
ashley.object('Title', 'Zoo');

const title = await ashley.resolve('Title');

By default the bound objects are passed by reference and thus everyone will receive the very same object and can possibly modified it. An alternative is to specify the clone option which will create a deep copy of the object each time it's requested.

ashley.object('FreshConfig', { port: 9001 }, {
  clone: true // uses https://lodash.com/docs/4.15.0#cloneDeep
});

Since some objects require special care when deep copying, it's possible to specify the function that should be used for the purpose.

ashley.object('Config', { port: 9001 }, {
  clone: function(obj) {
    // ...
    return copy;
  }
});

Note that you cannot specify the target using a file path since a string is also a valid target in itself.

ashley.object('Config', 'src/config');
await ashley.resolve('Config'); // => 'src/config'

Binding functions

When integrating with 3rd party frameworks or libraries, it's sometimes necessary to register callbacks which will later be invoked with a given set of parameters. For example when using Koa.

const Koa = require('koa');
const app = new Koa();

const ConsoleLogger = require('src/console_logger');
const logger = new ConsoleLogger();

app.use(async function Index(ctx, next) {
  logger.info(`Serving ${ctx.request.ip}`);
  ctx.body = 'Hello world';
});

// or
app.use(require('src/index'));

The goal here is to invoke the callback with not only the parameters provided by Koa (ctx and next) but also with configured dependencies, in this case an instance of ConsoleLogger. It's possible to take advantage of the function method as follows.

ashley.instance('Logger', 'src/console_logger');
ashley.function('Index', 'src/index', [Ashley._, Ashley._, 'Logger']);

app.use(await ashley.resolve('Index'));

Defining a function and passing it immediately afterwards is a very common pattern and can be shortened to just a single line. It takes advantage of the fact that binding a target returns an async function which resolves to the target.

app.use(await ashley.function('Index', 'src/index', [Ashley._, Ashley._, 'Logger']));

Notice the use of the Ashley._ placeholder. When present, it will be replaced with the parameter the callback was called with by the framework. In addition, Ashley will resolve the dependencies and pass all of it to the user defined function.

// src/index
module.exports = async function Index(ctx, next, logger) {
  logger.info(`Serving ${ctx.request.ip}`);
  ctx.body = 'Hello world';
}

It's possible to use the placeholder multiple times and in any order. If the callback is called with fewer parameters than expected, the remaining placeholders are passed in as undefined, followed by the declared dependencies. When the number of parameters is greater than expected, only tho

View on GitHub
GitHub Stars22
CategoryDevelopment
Updated6mo ago
Forks1

Languages

JavaScript

Security Score

72/100

Audited on Oct 6, 2025

No findings