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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sabl/context

v1.1.4

Published

JavaScript / TypeScript implementation of sabl/context pattern

Readme

codecov

version: 1.1.4 | tag: v1.1.4 | commit: df4fe1b3e | See Full docs on GitHub

@sabl/context

context is a pattern for injecting state and dependencies and for propagating cancellation signals. It is simple, mechanically clear, and intrinsically safe for concurrent environments. It was first demonstrated in the golang standard library context package. This package makes the same pattern available in TypeScript and JavaScript projects.

For more detail on the context pattern, see sabl / patterns / context.

Usage - Context Values

  1. Define a getter and setter

    You can use plain strings or public symbol keys with withValue and value, but using the following pattern is much better for both runtime and compile type safety:

    import { Maybe, IContext, Context, withValue } from '@sabl/context';
    import { MyService } from '$/services/my-service';
    
    // Do not export this value
    const ctxKeyMyService = Symbol('my-service');
    
    export function withMyService(ctx: IContext, svc: MyService):Context {
      return withValue(ctx, ctxKeyMyService, svc);
    }
    
    export function getMyService(ctx: IContext): Maybe<MyService> {
      return <Maybe<MyService>>ctx.value(ctxKeyMyService);
    }
  2. Set up your context

    Contexts are immutable. You add values or cancelation by wrapping a parent context with withValue or withCancel.

    import { Context } from '@sabl/context';
    import { MyService, withMyService, Logger, withLogger } from '$/services';
    
    // Make a root cancelable context 
    const [root, kill] = Context.cancel();
    
    // Give to a new logger instance
    const logger = new Logger(root);
    
    // Create child context with the logger injected
    let ctx = root.withValue(withLogger, logger);
    
    // Feed that to a new MyService and attach MyService to 
    // a new child context
    ctx = ctx.withValue(withMyService, new MyService(ctx));
    
    // Chaining works too
    ctx = ctx.withValue(withUser, user)
             .withValue('plain-string', 'hello');
  3. Retrieve a value

    import { getMyService, getLogger } from '$/services';
    
    export function exportData(ctx: Context, { /* other params */ }) {
      // Null is ok: Use getter directly
      let logger = getLogger(ctx);
      if (logger == null) {
        console.log('Warning: no logger. Falling back to console');
        logger = console;
      }
    
      // Null is not ok: Use require
      const svc = ctx.require(getMyService);
    
      // Check for cancelation if applicable
      if (ctx.canceled) {
        throw new Error('operation canceled');
      }
    
      /* ... do stuff with logger and svc ... */
    }

Usage - Cancellation

Create a root cancelable context with static Context.cancel(), or wrap an existing context with withCancel(ctx), which returns the child context along with a function that can be called to cancel it.

Note that all cancelable contexts must be canceled even if their work completes successfully. See library docs, pattern docs, original golang docs.

Cascade cancellation

Cancellation of an ancestor context is immediately cascaded down to all descendant contexts. Cancellation of a descendant context does not bubble up to an ancestor context.

const [root  , cancel      ] = Context.cancel();
const [child , cancelChild ] = withCancel(root);
const [gChild, cancelGChild] = withCancel(child);

// Cancellations do not bubble up
cancelGChild();
console.log(gChild.canceled); // true
console.log(child.canceled);  // false
console.log(root.canceled);   // false

// Cancellations do cascade down
cancel();
console.log(child.canceled);  // true
console.log(root.canceled);   // true

Check for cancellation errors

This library includes two Error types: CanceledError and DeadlineError. All DeadlineErrors are also CanceledErrors. Check whether an existing error or promise reject reason is due to cancellation using the static CanceledError.is and DeadlineError.is methods:

async function calculate(ctx: IContext, matrix: number[][]): Promise<number> {
  try {
    return await superCalc(ctx, matrix);
  } catch (e) {
    if (DeadlineError.is(e)) {
      // Operation specifically canceled due to a timeout
      ...
    } else if (CanceledError.is(e)) {
      // Operation was canceled due to some other reason
      ...
    } else {
      // Something else went wrong
      ...
    }
  }
}

Throw your own cancellation errors

Most existing libraries don't know about context. You can easily wrap an existing error or promise rejection reason with any of the following factory functions to create a CanceledError or DeadlineError:

CanceledError.as<T extends object>(reason: T): T
CanceledError.create(reason?: unknown): CanceledError

DeadlineError.as<T extends object>(reason: T): T
DeadlineError.create(reason?: unknown): DeadlineError

as

as requires a non-null input with typeof === 'object'. It decorates the object with a hidden property which is checked by CanceledError.is and DeadlineError.is.

const input = { name: 'my own object' };
const myError = CanceledError.as(input);
console.log(input === myError);         // true, it's the same object
console.log(CanceledError.is(myError)); // also true now
console.log(myError instanceof Error);  // false. It's the same plain object

create

create will wrap the input value, which may be null or undefined.

  • If input is null or undefined, a new CanceledError or DeadlineError is created with a default message
  • If input is as string, the string is used as the message for the new CanceledError or DeadlineError
  • If input is an Error that is already a CanceledError or DeadlineError, that the input itself is returned
  • If input is any other value with typeof === 'object' but is not a CanceledError or DeadlineError, then the input is used as the cause for a new CanceledError or DeadlineError
  • Any other input is rejected
// Empty
const err0 = DeadlineError.create(); // Same as `new DeadlineError()`;
console.log(err0.message); // 'Context deadline was exceeded'

// From a string
const err1 = DeadlineError.create('a message');
console.log(err1.message); // 'a message'

// From a decorated Error
const err2in   = DeadlineError.as(new Error('my own error'));
const err2out  = DeadlineError.create(err2in);
console.log(err2in === err2out);  // 'true'. Returned the same object

// From any other 'object' type
for(let input of [
  new Error('my own error'),
  new Date(),
  { a: 'b' }
]) { 
  const err = DeadlineError.create(input);
  console.log(input === err);        // 'false'
  console.log(input === err.cause);  // 'true'
}

Example: Wrapping errors as DeadlineError or CanceledError

const promise = someLibrary.doAThing();
promise.catch((reason) => {
  if (reason && reason.code == someLibrary.ERR_TIMEOUT) {
    throw DeadlineError.create(reason)
  } else if (reason && reason.code == someLibrary.ERR_OP_CANCELED_1230) {
    throw CanceledError.create(reason)
  }
  throw reason;
})

Example Use Cases

Testing

Consolidating all service injection into a single context parameter makes it easy and simple to provide a real instance in production, but a mocked or instrumented instance in testing. No fancy dependency injection frameworks required.

In production code

import { Context, withContext, getContext } from '@sabl/context';

/* -- service startup -- */
const app = new [express | koa | etc.]();
 
// Build up shared services to inject
const ctx = Context.background.
  withValue(withRepo, new RealRepo()).
  withValue(withLogger, new RealLogger()).
  withValue(with..., new ...()) 
  /* etc */; 

// Attach context to each incoming request
app.use((req, res, next) => {
  return next(withContext(req, ctx), res);
})

/* -- export data route -- */ 
import { exportData } from '$/export-service'

app.use('data/export', async (req, res) => { 
  const exportParams = parseBody(req.body);
  const ctx = getContext(req);
  // Pass along context to service logic
  const data = await exportData(ctx, exportParams);
  res.json(data);
})

In test code

import { exportData } from '$/export-service'

describe('export-service', () => {
  describe('exportData', () => {
    it('logs record count', async () => {
      const mockRepo = new MockRepo();
      const logger   = new MockLogger();
      const ctx = Context.background.
        withValue(withRepo, mockRepo).
        withValue(withLogger, mockLogger).
        withValue(with..., new ...());

      mockRepo.mock('getRecords', () => {
        return [ ... ]
      })

      // Provide context with mocked / alternative 
      // services to method under test
      await exportData(ctx, {
        format: 'csv', 
        paginate: false
      });

      expect(mockLogger.messages).toContain('Exported 5 records');
    })
  })
})

Proxy authentication

A common item to inject in a context is the current authenticated user. This makes it easy to inject fake or test users in testing, but it also makes it easy to implement proxy authentication in production scenarios, such as allowing admins to interact with an application as another user in order to reproduce an issue exactly as the target user sees it.

app.use(async (req, res, next) => {
  const proxyUserId = eq.headers['X-Proxy-User'];

  if(!proxyUserId || !proxyUserId.length)
    return next(req, res);
   
  const ctx = req.context as Context;

  // Get already authenticated user from incoming context
  const realUser = getUser(ctx);
  if(realUser == null) {
    throw new SecurityError('No underlying user authenticated');
  };

  // Get needed service from context
  const secSvc = ctx.require(getSecSvc);
  const targetUser = await secSvc.findUser(proxyUserId);
  if(targetUser == null) {
    throw new SecurityError('Target user does not exist');
  }

  const ok = await secSvc.canProxy(realUser, targetUser);
  if(!ok) { 
    throw new SecurityError(
      `User ${realUser.userName} not authorized to proxy user ${targetUser.userName}`
    );
  } 

  // Continue pipeline with augmented context and request
  const childCtx = ctx.
    withValue(withUser, targetUser).      // Replace User with proxied target user
    withValue(withRealUser, realUser).    // But also attach real user as RealUser
    withValue(withIsProxied, true);       // And flag that context is proxied

  return next(req.withContext(childCtx), res);
})

Database Transactions

Occasionally a code path requires several successive database actions to succeed or fail together in a transaction. This becomes especially tricky if the same code path might sometimes be executed within a transaction while other times not, or when a code path could result in a nested attempts to initiate a remote connection. All of this can be greatly simplified in code that needs to execute database calls by injecting the transaction, if any, in the context.

async function addItemRecord(ctx: Context, { /* other params */ }) {
  await execTxn(ctx, async (ctx, qry) => {
    /* do something with inner ctx and qry, 
       which execute in a database transaction */

    // Within this transaction, call another method 
    // which may itself include a transaction:
    await addItemStock(ctx, { /* ... */ });
  });
}

async function addItemStock(ctx: Context, { /* other params */ }) {
  await execTxn(ctx, async (ctx, qry) => {
    /* do something with inner ctx and qry, 
       which execute in a database transaction */
  });
}
 
// Generic implementation of execTxn with simple RDB interfaces:
interface Query {
  async query(ctx: Context, sql: string, params: ...): Promise<...>;
  async exec(ctx: Context, sql: string, params: ...): Promise<number>; 
}
 
interface Db extends Query {
  async beginTxn(ctx: Context): Promise<DbTxn>;
}

interface DbTxn extends Query {
  async commit(): Promise<void>;
  async rollback(): Promise<void>;
}
 
type QueryFunc = (ctx: Context, qry: Query) => Promise<void>;

async function execTxn(ctx: Context, fn: QueryFunc) {
  let txn = getDbTxn(ctx);
  if(txn != null) { 
    // Already in a transaction. Execute the callback within this one
    return fn(ctx, txn);
  }

  const db = ctx.require(getDb);
  txn = await db.beginTxn(ctx);
  try {
    const txnCtx = ctx.withValue(withDbTxn, txn);
    await fn(txnCtx, txn);
    await txn.commit();
  } catch {
    await txn.rollback();
    throw;
  }
}