Composition API pattern implementation for vanilla JavaScript libraries with context injection, async support, and namespace management.
—
Context creation and management provides the core functionality for creating typed context instances that can inject values throughout a call stack.
/**
* Creates a new context instance with optional async context support
* @param opts - Configuration options for the context
* @returns Context instance with getter, setter, and execution methods
*/
function createContext<T = any>(opts: ContextOptions): UseContext<T>;
interface UseContext<T> {
/**
* Get the current context. Throws if no context is set.
* @throws Error when context is not available
*/
use(): T;
/**
* Get the current context. Returns null when no context is set.
* @returns Current context instance or null if not set
*/
tryUse(): T | null;
/**
* Set the context as Singleton Pattern.
* @param instance - The context instance to set
* @param replace - Whether to replace existing conflicting context
* @throws Error if context conflict occurs and replace is false
*/
set(instance?: T, replace?: boolean): void;
/**
* Clear current context.
*/
unset(): void;
/**
* Execute a synchronous function with the provided context.
* @param instance - Context instance to provide during execution
* @param callback - Function to execute with context
* @returns Result of callback execution
* @throws Error if context conflict occurs
*/
call<R>(instance: T, callback: () => R): R;
/**
* Execute an asynchronous function with the provided context.
* Requires installing the transform plugin to work properly.
* @param instance - Context instance to provide during execution
* @param callback - Async function to execute with context
* @returns Promise resolving to callback result
*/
callAsync<R>(instance: T, callback: () => R | Promise<R>): Promise<R>;
}
interface ContextOptions {
/**
* Enable async context support using AsyncLocalStorage
*/
asyncContext?: boolean;
/**
* AsyncLocalStorage implementation for async context
*/
AsyncLocalStorage?: typeof AsyncLocalStorage;
}import { createContext } from "unctx";
interface User {
id: number;
name: string;
email: string;
}
// Create a typed context
const userContext = createContext<User>();
// Execute code with context
const result = userContext.call(
{ id: 1, name: "Alice", email: "alice@example.com" },
() => {
// Context is available throughout the call stack
const currentUser = userContext.use();
console.log(`Processing user: ${currentUser.name}`);
// Call other functions that can access the same context
return processUserData();
}
);
function processUserData() {
// Context is still available in nested functions
const user = userContext.use();
return `Processed data for ${user.email}`;
}const myContext = createContext<string>();
// This will throw an error - no context set
try {
myContext.use();
} catch (error) {
console.error("Context not available:", error.message);
}
// Safe usage - returns null instead of throwing
const safeValue = myContext.tryUse(); // nullconst ctx = createContext<string>();
ctx.call("A", () => {
// This will throw "Context conflict" error
try {
ctx.call("B", () => {
console.log("This won't execute");
});
} catch (error) {
console.error(error.message); // "Context conflict"
}
});For shared instances that don't depend on request context:
import { createContext } from "unctx";
interface AppConfig {
apiUrl: string;
version: string;
}
const configContext = createContext<AppConfig>();
// Set global configuration
configContext.set({
apiUrl: "https://api.example.com",
version: "1.0.0"
});
// Use throughout the application
function makeApiCall() {
const config = configContext.use();
return fetch(`${config.apiUrl}/data`);
}
// Clear singleton when needed
configContext.unset();
// Replace singleton configuration
configContext.set(newConfig, true); // true = replace existingimport { createContext } from "unctx";
import { AsyncLocalStorage } from "node:async_hooks";
interface RequestContext {
requestId: string;
userId: number;
}
const requestContext = createContext<RequestContext>({
asyncContext: true,
AsyncLocalStorage
});
// Context persists across async boundaries
await requestContext.callAsync(
{ requestId: "req-123", userId: 42 },
async () => {
console.log(requestContext.use().requestId); // "req-123"
await new Promise(resolve => setTimeout(resolve, 100));
// Context is still available after await
console.log(requestContext.use().requestId); // "req-123"
await processRequest();
}
);
async function processRequest() {
// Context available in async nested functions
const ctx = requestContext.use();
await fetch(`/api/users/${ctx.userId}`);
}const ctx = createContext<string>();
// Context is properly cleaned up even if callback throws
try {
ctx.call("test", () => {
throw new Error("Something went wrong");
});
} catch (error) {
console.error("Callback failed:", error.message);
}
// Context is cleared, can use with different value
ctx.call("new-value", () => {
console.log(ctx.use()); // "new-value"
});Install with Tessl CLI
npx tessl i tessl/npm-unctx