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-container

v1.0.2

Published

A context container for TypeScript. Allows a 'Context' object to be shared throughout the lifecycle of a 'job', such as a web request, script execution, or task handler.

Downloads

6

Readme

A server-side context container for TypeScript. Allows a 'Context' object to be shared throughout the lifecycle of a 'job', such as a web request, script execution, or task handler.

You create the context container (CC) at the beginning of each job, and then pass it as an argument to all your functions and classes. They can then use it to access shared code, state, cached results, and the time that the job started.

There are currently four features:

  1. Plugins
  2. Singletons
  3. Memoization
  4. Request timestamp

Installation

yarn add context-container

or

npm install context-container

Features

Plugins

Plugins allow you to write your high-level application code in a way that specifies interfaces for lower-level functionality that needs to exist for the application code to run, without having the high-level code depend on the low-level code. This is to support the ideas of Clean Architecture.

To use a plugin, first, create the definition of the plugin type and an object that will be used to reference it. The purpose of the "reference" object is to have something that high-level code can point to without knowing about the implementation of the plugin.

import { createPluginReference } from 'context-container';

export type SerializeObjectPluginType = {
  serialize: (object: unknown) => string;
};

export const SerializeObjectPlugin = createPluginReference<SerializeObjectPluginType>();

Then write your code that needs to use the plugin without caring how it's implemented:

import { CC } from 'context-container';
import { SerializeObjectPlugin } from 'your/code/SerializeObjectPlugin';

function doSomething(cc: CC, data: unknown) {
  const serializedData = cc.getPlugin(SerializeObjectPlugin).serialize(data);
  ...
}

Then write your implementation of the plugin:

import { SerializeObjectPluginType } from 'your/code/SerializeObjectPlugin';

export const SerializeObjectPluginImpl: SerializeObjectPluginType = {
  serialize: (object: unknown) => JSON.stringify(object),
};

Finally, link the plugin reference and implementation when the context container is created. This should happen at the beginning of handling your job (web request, script, task executor, etc.)

import { ContextContainerFactory } from 'context-container';
import { SerializeObjectPlugin } from 'your/code/SerializeObjectPlugin';
import { SerializeObjectPluginImpl } from 'your/code/SerializeObjectPluginImpl';

function main() {
  const cc = ContextContainerFactory.create([
    {
      reference: SerializeObjectPlugin,
      implementation: SerializeObjectPluginImpl
    },
  ]);
  executeProgram(cc, args);
}

Singletons

A Contextual Singleton is a class that has one instance per Context Container. The usage pattern for Context Containers is to create a new one at the beginning of handling each job (e.g., web request, script, task executor, etc.) This means that for ContextualSingletons, you will have one instance per job run, and different jobs will not share instances of this the class.

Define the singleton class:

class AuthenticatedViewer extends ContextualSingleton {
  private viewer: Viewer | undefined;
  public set(viewer: Viewer) { this.viewer = viewer; }
  public get(): Viewer {
    const viewer = this.viewer;
    if (viewer == null) {
      throw new Error('Viewer not set');
    }
    return viewer;
  }
}

In this example, we will initialize the data at the beginning of the job and then access it several times throughout the job. At the beginning of the job:

const cc = ContextContainerFactory.create([...plugins]);
...
const viewer = authenticateUser(...);
cc.getSingleton(AuthenticatedViewer).set(viewer);
executeProgram(cc, args);

Later on in the job:

function doSomething(cc: ContextContainer) {
  const viewer = cc.getSingleton(AuthenticatedViewer).get();
  ...
}

Memoization

Memoization here is done in the context of a particular job (web request, script, task executor, etc.). That means memoized values are not shared between different jobs, and are discarded at the end of the job. If your jobs are short-lived, this is usually a reasonable way of avoiding having stale data in your memoization cache.

This also means that if your job is done with the same user permissions for the entire job, then you can memoize results that are subject to user permission checks. E.g., if user A is logged in, and they attempt to access resource X, then you can memoize the value that is returned to them, which includes both the resource X and the fact that they are allowed to access it.

However, if you do this, be careful about any resources that may change while the job is running, particularly if the job may change the resource or the permissions associated with it.

As a more cautious alternative, you can memoize the fetched resource (perhaps as a permission-less database proxy object), and then check the permissions on the resource every time you access it.

Defining a memoized function:

class UserEmailPreferences {
 public static async load(
   cc: CC,
   userID: string,
 ): Promise<UserEmailPreferences> {
   const dbProxy = await cc.memoize(
     this,
     userID,
     async () => await cc
       .getPlugin(DatabasePlugin)
       .loadUserEmailPreferences(userID),
   );
   if (!(await UserEmailPreferencesPrivacyImpl.canRead(cc, userID, dbProxy))) {
     throw new PermissionError();
   }
   return new UserEmailPreferences(cc, userID, dbProxy);
 }
}

Using the memoized function:

async function doSomething(cc: CC, userID: string) {
  const userEmailPreferences = await UserEmailPreferences.load(cc, userID);
  ...
}

Timestamp

Timestamp is included here because it is common to use a single timestamp as the canonical time that things occurred in a job (web request, script, task executor, etc.).

For example, if you are processing a web request, you may want to use the timestamp that the request started as the updatedTime when updating an object in persistent storage.

If updating multiple objects, you may want to use the same timestamp for all of them, rather than the exact timestamp that the code happened to be running at when handling that particular object.

If you want to customize the timestamp, rather than using the time that the ContextContainer was created, you can pass a timestamp as an argument to ContextContainerFactory.create().

Timestamp could have been implemented as a Singleton. In fact, that's how it was originally implemented. But because it's so common and so useful, it's worth having a dedicated API for it. If you're considering attaching similar data to the ContextContainer, consider using a Singleton instead. See the Singleton section above for how to do this.