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

context-tree

v0.2.2

Published

Simple hierarchical dependency injection library, as the part of Fractal Architecture

Downloads

11

Readme

context-tree is a simple implementation of a hierarchical dependency injection (DI) pattern for building scalable web applications. It implements the same concept as React's Context, but without relying on React. The pattern allows you to create arbitrary nested applications and sub-applications, simplifying the architecture and maintaining code clarity.

Unlike other DI frameworks, context-tree does not require you to define a dependency graph in advance, offering a more flexible approach. The difference from the other DI frameworks is an inherent hierarchy of injectable entities, called contexts. They align with the hierarchy of the application itself and form a tree like React contexts do. Like React, you can redefine the context value at any point of the tree, and also add and remove context resolvers dynamically.

context-tree is framework-agnostic and can be used with any framework or without a framework at all. It also does not require any decorators or other magic, so it's easy to understand and debug, and can be used in pure JS projects. context-tree has no dependencies and is very lightweight, and because of the pattern simplicity, it's very fast.

Defining models

Context-tree requires your data models to implement a simple IContext interface with only one field required: context of type IContext | IContext[] | null | undefined. This field should point to a parent or multiple parent models, or be null in case there is no parent model.

Here's a simple example:

import { IContext } from "context-tree";

class RootModel implements IContext {
  // root model does not have context, so it's null
  public context = null;

  // by convention, all models with context take a parent as the first argument
  childModel = new ChildModel(this);
}

class ChildModel implements IContext {
  // use TS shortcut to define a field from the constructor arg
  // now it points to the parent model
  constructor(public context: IContext) {}
}

At this point, we have a model tree. Let's define a context and add the resolver to it.

import { Context, IContext } from "context-tree";

// define some interface for config object
interface IConfigModel {
  baseUrl: string;
  apiKey: string;
}

// an implementation of the interface
class ConfigModel implements IConfigModel {
  baseUrl = "https://.../";
  apiKey = "abcdef123";
}

// define a context that carries the type of the config
const ConfigContext = new Context<IConfig>("ConfigContext");

class RootModel implements IContext {
  public context = null;

  // define resolvers - functions that are called when the context resolves
  public contextResolvers = Context.resolvers([
    ConfigContext.resolvesTo(() => this.config),
  ]);

  private config = new ConfigModel();

  // pass this as the context of the child model
  private childModel = new ChildModel(this);
}

class ChildModel implements IContext {
  constructor(public context: IContext) {}

  async getData() {
    // to get the config instance, call `resolve` on the context
    // and pass the current model as the first argument
    const config = ConfigContext.resolve(this);

    // use the config instance
    const data = await fetch(`${config.baseUrl}/endpoint`);
  }
}

In case you want a model itself to be a context, you can use contextType field:

const RootContext = new Context<RootModel>('RootContext');

class RootModel implements IContext {
  // no parent context
  context = null;

  // now RootContext resolves to the model instance
  contextType = RootContext;

  // define some extra resolvers
  contextResolvers = Context.resolvers([
    ...
  ]);
}

Partial contexts

Not always full contexts are needed. For example, the config model and its interface can contain dozens of fields, and our model may need only a few of them. When we are writing unit tests for a model, we have to supply a full config context that implements every field from its interface, and that can be cumbersome.

Partial contexts solve this problem by allowing you to define a partial interface from your original context. If the partial context resolves, it will resolve to the closest parent model that implements the partial interface.

Here's an example:

interface IConfigModel {
  baseUrl: string;
  option1: string;
  option2: string;
  option3: string;
}

const ConfigContext = new Context<IConfigModel>("ConfigContext");

// pick only option1 and option2 from IConfigModel
type IPartialConfigModel = Pick<IConfigModel, "option1" | "option2">;

// define a partial context derived from the ConfigContext
const PartialConfigContext = ConfigContext.partial<IPartialConfigModel>(
  "PartialConfigContext"
);

// Finds a closest instance of IPartialConfigModel or IConfigModel
PartialConfigContext.resolve(this);

Dynamic context manipulation

Context resolvers can be dynamically added or removed from a model. This might be useful in complex scenarios when contexts are not known in advance.

const Context1 = new Context<number>("Context1");
const Context2 = new Context<string>("Context2");

class RootModel implements IContext {
  // no parent context
  context = null;

  // define static resolvers
  contextResolvers = Context.resolvers([Context1.resolvesTo(() => 1 + 2)]);

  // add dynamic resolvers
  doSomething() {
    this.contextResolvers.addResolver(Context2.resolvesTo(() => "hello"));
  }

  // remove dynamic resolvers
  doSomethingElse() {
    this.contextResolvers.removeResolver(Context2);
  }
}

Required contexts

Sometimes you want to make sure that a model has all required contexts resolved. For example, you may want to make sure that a model has a config context resolved before it can be used. To do that, you can define a static field requiredContexts on a class or class instance:

const Context1 = new Context<number>("Context1");
const Context2 = new Context<string>("Context2");

class RootModel implements IContext {
  // no parent context
  context = null;

  // define resolvers
  contextResolvers = Context.resolvers([Context1.resolvesTo(() => 1 + 2)]);

  // define required contexts
  // RootModel has no Context2 resolver, so it will throw an error
  static requiredContexts = [Context2];
}

// throws an error
Context.checkRequired(new RootModel());

API

Models

Each model should implement the IContext interface:

interface IContext {
  context: IContext | IContext[] | null | undefined;
  contextType?: Context<any>;
  contextResolvers?: ContextResolvers;
}

The usual way to pass the required context field is the first argument of the constructor:

class Model implements IContext {
  constructor(public context: IContext) {}
}

Context

new Context<T>(name: string): Context<T>

Creates a new context with the given name. The name is used for debugging purposes.

contextInstance.partial<T>(name: string): Context<T>

Creates a partial context derived from the current context. The partial context can be resolved to the closest parent model that implements the partial interface.

Context.resolvesTo<T>(resolver: () => T): ContextResolver<T>

Create a resolver for the context. The resolver is a function that returns a value of type T. The resolver is called when the context is resolved.

Context.resolvers(resolvers: Array<ContextResolver<any>>): ContextResolvers

Define a list of resolvers for a model.

contextInstance.resolve<T>(model: IContext): T

Finds the closest context resolver of the type and calls it to resolve the value. If no resolver is found, throws an error.

contextInstance.resolveMaybe<T>(model: IContext): T | undefined

Finds the closest context resolver of the type and calls it to resolve the value. If no resolver is found, returns undefined.

contextInstance.findResolver(model: IContext): ContextResolver<any> | undefined

Finds the closest context resolver of the type. If no resolver is found, returns undefined.

Context.checkRequired(model: IContext): void

Checks if all required resolvers are defined for the model. If not, throws an error. Required contexts are defined by requiredContexts field on a class or class instance.

Author

Eugene Daragan

License

MIT