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

sistema

v0.0.10

Published

Dependency injection library

Downloads

12

Readme

Sistema

Sistema is a lightweight dependency injection library for node.js. It makes possible to write fast, testable, observable and reliable applications (check these claims at the bottom!).

Dependency

The core concept of sistema is the dependency:

const { Dependency } = require("sistema")
const { Client } = require("pg")

const dbConnection = new Dependency().provides(async () => {
  const client = new Client()
  // Connect to the PostgreSQL database
  return client.connect()
})

A dependency runs a function and provides a value. The value could be either a regular function of a function returning a promise.

A dependency can depend on other dependencies (just one in the example but they can be multiple!):

const usersQuery = new Dependency()
  .dependsOn(dbConnection)
  .provides(async (client) => {
    const result = await client.query("SELECT * FROM users")
    return result.rows
  })

A dependency is executed with the run method:

const rows = await usersQuery.run()
rows.forEach((row) => console.log(row))

The return value of run is always a promise.

Parameters

A dependency can take parameters, these are expressed as strings (or Symbols):

const userQuery = new Dependency()
  .dependsOn(dbConnection, "userId")
  .provides(async (client, userId) => {
    const result = await client.query("SELECT * FROM users WHERE ID = $1", [
      userId,
    ])
    return result.rows[0]
  })

and must be passed in the run method as object values.

await userQuery.run({ userId: 12345 })

ResourceDependencies and context

In the previous example we opened a database connection every time we needed a dbConnection. Dependencies like that should behave like resources: they are created once, used as many times as needed and then disposed (closing a database connection, for example). We call them ResourceDependencies:

const { ResourceDependency } = require("sistema")
const { Client } = require("pg")

let client

const dbConnection = new ResourceDependency()
  .provides(async () => {
    client = new Client()
    // Connect to the PostgreSQL database
    return client.connect()
  })
  .disposes(() => {
    client.end()
  })

This way the connection is established only the first time and reused across multiple usages of run.

The dependency/resourceDependency executions are preserved in an object called defaultContext. This exposes a method that allows to shutdown all dependencies in the right order.

const { defaultContext } = require("sistema")

await userQuery.run({ userId: 12345 })
// ...
await defaultContext.shutdown() // this shuts down all dependencies that have been executed in the default context

The shutdown respects the order in which dependencies are connected so for example, we ensure that all queries to a database are through before closing the db connection.

  • A Dependency shuts down when there are no in-flights calls to the function provided.
  • A ResourceDependency shuts down when the dispose function ran its course.

Once a Dependency or a ResourceDependency are shut down, they no longer work and return an exception when called.

Reset

It is possible to reset a graph of dependencies so that all ResourceDependencies are closed (their "dispose" function is called), but they can still be used and recreated.

await defaultContext.reset()

Reset can be called on an individual resourceDependency as well:

await dbConnection.reset()

Multiple contexts

When dealing with dependencies that are part of different lifecycles you can use more than one context. So that shutting down (or resetting) a group of dependencies doesn't shut down dependencies that are used in another context. The new context need to be passed to run:

const { Context } = require("sistema")

const customContext = new Context()

await userQuery.run({ userId: 12345 }, customContext)
// ...
await customContext.shutdown() // this shuts down all dependencies that have been executed in customContext

If a dependency belongs to multiple context, it is only shutdown after all context it belongs shut down.

Run multiple dependencies at once

Theoretically, Promise.all can be used to run multiple dependencies at once:

const [a, b] = await Promise.all([depA.run(), depB.run()])

This should return the correct result (if the dependencies are pure functions). But common dependencies can be executed multiple times. To avoid this, you can use run:

const { run } = require("sistema")
const [a, b] = await run([depA, depB])

"run" can also be used to run a single dependency:

depA.run()
// is equivalent to
run(depA)

Observability

Sistema has some facility to help observe how the system works and to make debugging and logging easier.

Names

Both Dependency, ResourceDependency and Context, can have a descriptive name:

const userQuery = new Dependency('User query')...

That can be read in the name attribute:

console.log(userQuery.name) // 'User query'

Context events

A context can be configured with event handlers that are executed when a dependency is executed with success or fail. Same for the shutdown and reset.

const { CONTEXT_EVENTS } = require("sistema")

const context = new Context("main context")
  .on(
    CONTEXT_EVENTS.SUCCESS_RUN,
    ({ dependency, context, timeStart, timeEnd }) => {
      // example: 'User query ran by the main context in 14 ms'
      console.log(
        `${dependency.name} ran by the ${context.name} in ${
          timeEnd - timeStart
        } ms`
      )
    }
  )
  .on(
    CONTEXT_EVENTS.FAIL_RUN,
    ({ dependency, context, timeStart, timeEnd, error }) => {
      console.log(
        `${dependency.name} ran with Error (${error.message}) by the ${
          context.name
        } in ${timeEnd - timeStart} ms`
      )
    }
  )

It is also possible to add events to the defaultContext. Here is a list of the events:

| Events | Parameters | | ---------------- | ------------------------------------------------------------- | | SUCCESS_RUN | dependency, context, timeStart, timeEnd, _executionId | | FAIL_RUN | dependency, context, timeStart, timeEnd, _executionId, error | | SUCCESS_SHUTDOWN | dependency, context, timeStart, timeEnd, _executionId | | FAIL_SHUTDOWN | dependency, context, timeStart, timeEnd, _executionId, error | | SUCCESS_RESET | dependency, context, timeStart, timeEnd, _executionId | | FAIL_RESET | dependency, context, timeStart, timeEnd, _executionId, error |

And the parameters:

  • dependency: the dependency object
  • context: the context object
  • timestart, timeEnd: timeStamp when a dependency started/ended the process
  • _executionId: id that identifies a single run/shutdown/reset execution, that is unique within all dependencies involved
  • error: the error thrown

Dependencies attributes

Dependencies have extra attributes and methods that help with the debugging:

const dep = new ResourceDependency("Test")
dep.toString() // returns "ResourceDependency Test"
dep.getEdges() // returns the dependencies as an array
dep.getInverseEdges() // returns all the dependents as an array

Adjacency list

In case is required to have a list of all dependencies connected you can use getAdjacencyList.

const { getAdjacencyList } = require("sistema")
const a = new Dependency()
const b = new Dependency().dependsOn(a)
const c = new Dependency().dependsOn(a, b)

getAdjacencyList(a) // [a]
getAdjacencyList(b) // [b, a]
getAdjacencyList(c) // [c, b, a]

getAdjacencyList works also with an array of dependencies. Context and dependencies have a getAdjacencyList method. Dependency.prototype.getAdjacencyList is a shorthand to run getAdjancencyList with a single dependency. Context.prototype.getAdjacencyList returns all dependencies that have been executed so far in the context. Here is an example on how to use getAdjacencyList to print the adjacency list in JSON, for example:

const adj = {}
for (const d of dep.getAdjacencyList()) {
  adj[d.name] = d.getEdges().map((d) => d.name)
}
console.log(JSON.stringify(adj))

Meta dependency

It is a special dependency that shows information about the execution. It contains a field timings with the execution order and timing of the dependencies executed before.

const { META_DEPENDENCY } = require("sistema")
const [myDependencyValue, { timings }] = await run([
  myDependency,
  META_DEPENDENCY,
])

timings is an array of objects. Every object has:

  • context: the context used
  • dependency: the dependency that was executed
  • timeStart: the time when the dependency started its execution
  • timeEnd: the time when the dependency ended its execution
  • _executionId: id that identifies a single run/shutdown/reset execution, that is unique within all dependencies involved You can use META_DEPENDENCY as a regular dependency as well.

Execution id

You can use the execution id to keep the relation between all dependencies called when run is invoked. By default is a UUID generated on each execution. But it can also be passed:

const { EXECUTION_ID } = require("sistema")

const b = new Dependency().dependsOn(a, EXECUTION_ID).provides((a, id) => {
  // ...
})

const [EXECUTION_ID] = await run({ [EXECUTION_ID]: "myid" })

Testability

Sistema improves the testability of the codebase because, taking care of wiring dependencies between them, it leaves simple dependencies that can be tested in isolation. Mocking a dependency is super easy. Just pass it in the run method using a Map:

const args = new Map([
  [userId, 12345],
  [dbConnection, connectionMock],
])
await userQuery.run(args)

connectionMock will be used instead of dbConnection. This can be used to mock some or even all of the dependencies in the dependency graph. To implement unit and integration tests.

Sistema cookbook

Collecting here some use cases and patterns.

Shutting down a server

This is an example on how to listen to the signals and call shutdown on the defaultContext to ensure the application closes gracefully, without interrupting pending tasks.

const { defaultContext } = require("sistema")
const events = { SIGTERM: 0, SIGINT: 0, unhandledRejection: 1, error: 1 }

console.log("Press CTRL+C to stop")

Object.keys(events).forEach((name) => {
  process.on(name, async (events) => {
    await defaultContext.shutdown()
    process.exit(events[name])
  })
})

Warm up a resourceDependency

All resource dependencies are invoked when requested and their result is cached for subsequent calls. This means, for example, that the database connection stored in a resource dependency is opened only the first time a query is called. If we prefer that this happens at the application start up we can run the resource dependency right after is defined:

const { ResourceDependency } = require("sistema")
const { Client } = require("pg")

let client

const dbConnection = new ResourceDependency()
  .provides(async () => {
    client = new Client()
    // Connect to the PostgreSQL database
    return client.connect()
  })
  .disposes(() => {
    client.end()
  })

dbConnection.run() // no need to await!

module.exports = dbConnection

Add server timing header

Server timing is an header that allows to send to the user agent (the browser) the time spent in different operations. It can be used to visualise the timing for the execution of the dependencies.

function writeServerTiming(timings) {
  const timingsString = timings
    .map(
      ({ dependency, timeStart, timeEnd }) =>
        `${dependency.name};dur=${(timeEnd - timeStart).toFixed(2)}`
    )
    .join(",");
  return timingsString
}
...
const [myDep, { timings }] = await run([myDependency, META_DEPENDENCY], {
  req,
  res,
});
const timingsHeader = writeServerTiming(timings);
// this needs to be added to the Server-Timing header

Use with a web framework

Sistema integrates easily with any web framework (like Express.js, Fastify etc.). The suggested approach is to use the dependency to wrap the controller. For example with express:

app.get("/test", async (req, res) => {
  await test.run({ req, res })
})

Visualise dependencies

Sistema lens can be used to show graphically how dependencies are wired together and display further information. It is designed to make code base much easier to understand.

Sistema Design principles

Sistema (Italian for "system") allows to express an application as a directed acyclic graph of functions. It executes the graphs of functions so that the dependencies constraint is respected. The algorithm is a derivative of DFS similar to topological sorting that walks multiple graph edges in parallel. In the same way is possible to shutdown the dependencies in the inverse order.

Graph example

Sistema does one thing well. It integrates with other libraries rather than being an invasive framework. It has no dependencies and only a small amount of dev dependencies. It uses types but no transpilation for the best dev experience.

Sistema is:

  • FAST: dependencies are executed in parallel, in the optimal order and only once every execution
  • TESTABLE: Sistema takes care of the wiring, so that dependencies can be tested in isolation
  • OBSERVABLE: Sistema has simple entry points to add logging/tracking and makes easy to inspect how the dependencies are connected
  • RELIABLE: Sistema takes care of shutting dependencies in the right order