Skip to main content

Execution Context

The @daiso-tech/core/execution-context module provides a type-safe, composable, and environment-agnostic way to store and propagate contextual data (such as request IDs, user info, or tracing metadata) across async boundaries and function calls. It is inspired by thread-local storage and context propagation in distributed systems, but is designed for modern TypeScript/JavaScript applications.

Initial configuration

To begin using the execution context, you'll need to create and configure an instance:

import { ExecutionContext, contextToken } from "@daiso-tech/core/execution-context";
import { AlsExecutionContextAdapter } from "@daiso-tech/core/execution-context/als-execution-context-adapter";

// Create an execution context instance with an adapter
const executionContext = new ExecutionContext(new AlsExecutionContextAdapter());

Execution context basics

Running code with context

You can run code within a context boundary, and all context values will be accessible throughout the call chain:

import { Namespace } from "@daiso-tech/core/namespace";

// Define context tokens using namespaced IDs to avoid collisions
const namespace = new Namespace("myapp");
const userToken = contextToken<{ id: string; name: string }>(namespace.id("user"));
const requestIdToken = contextToken<string>(namespace.id("requestId"));

function logData(): void {
// Access context values later in the call chain

// { id: "123", name: "Alice" }
const user = executionContext.get(userToken);
// "req-456"
const reqId = executionContext.get(requestIdToken);

console.log("user:", user);
console.log("reqId:", reqId);
}

executionContext.run(() => {
executionContext.put(userToken, { id: "123", name: "Alice" });
executionContext.put(requestIdToken, "req-456");
logData();
});

Binding functions to context

You can bind a function to the current context, so it always executes with the captured context values:

executionContext.run(() => {
executionContext.put(userToken, { id: "123", name: "Alice" });
executionContext.put(requestIdToken, "req-456");

const logData = executionContext.bind((msg: string): void => {
// Access context values later in the call chain
const user = executionContext.get(userToken); // { id: "123", name: "Alice" }
const reqId = executionContext.get(requestIdToken); // "req-456"
console.log("message:", msg)
console.log("user:", user);
console.log("reqId:", reqId);
});

logData("hello");
});

Patterns

Type safety with context tokens

You can enforce compile-time type safety by defining context tokens with specific types:

const userToken = contextToken<{ id: string; name: string }>("user");
executionContext.put(userToken, { id: "123", name: "Alice" });
// TypeScript will error if you try to put a value of the wrong type.

Immutable and chainable context operations

All context mutation methods return the context instance, allowing for method chaining:

executionContext
.put(userToken, { id: "123", name: "Alice" })
.put(requestIdToken, "req-456");

Conditional context updates

You can conditionally update the context:

executionContext.when(
true,
(ctx) => ctx.put(userToken, { id: "conditional", name: "Bob" })
);

Adapters

  • AlsExecutionContextAdapter: Uses Node.js AsyncLocalStorage for async context propagation.
  • NoOpExecutionContextAdapter: No-op adapter for testing or environments without async context support.

Separating reading, updating, and execution concerns

The library includes several contracts that separate concerns for different use cases:

  • IReadableContext: Read-only access to context values (safe for consumers that should not mutate context).
  • IContext: Adds all mutation methods (put, update, remove, etc.) for full context management.
  • IExecutionContextBase: Adds execution boundary methods (run, bind) for context propagation and isolation.
  • IExecutionContext: Combines all of the above for complete context management and execution.

IExecutionContextBase

  • run(invokable) — Runs a function within the current execution context. All context values are accessible during execution.
  • bind(fn) — Returns a new function that, when called, executes the original function within the captured context.

IContext

  • add(token, value) — Adds a value only if it doesn't already exist. No-op if the key exists.
  • put(token, value) — Sets or overwrites a value for the token.
  • putIncrement(token, settings?) — Initializes (if missing) and increments a numeric value. Optional max cap.
  • putDecrement(token, settings?) — Initializes (if missing) and decrements a numeric value. Optional min floor.
  • putPush(token, ...values) — Initializes (if missing) and pushes values to an array.
  • update(token, value) — Updates a value only if it exists. No-op if missing.
  • updateIncrement(token, settings?) — Increments a numeric value only if it exists. Optional max cap.
  • updateDecrement(token, settings?) — Decrements a numeric value only if it exists. Optional min floor.
  • updatePush(token, ...values) — Pushes values to an array only if it exists. No-op if missing.
  • remove(token) — Removes a value from the context.
  • when(condition, ...invokables) — Conditionally applies operations if the condition is true.

IReadableContext

  • contains(token, matchValue) — Checks if an array context value contains a specific item or matches a predicate.
  • exists(token) — Checks if a value exists for the token.
  • missing(token) — Checks if a value is missing for the token.
  • get(token) — Retrieves a value or null if not found.
  • getOr(token, defaultValue) — Retrieves a value or returns the provided default if not found.
  • getOrFail(token) — Retrieves a value or throws if not found.

Further information

For further information refer to @daiso-tech/core/execution-context API docs.