CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-unctx

Composition API pattern implementation for vanilla JavaScript libraries with context injection, async support, and namespace management.

Pending
Overview
Eval results
Files

namespace-management.mddocs/

Namespace Management

Namespace management provides a global context registry system to avoid conflicts between different library versions and enable context sharing across modules.

API Reference

/**
 * Create a namespace for managing multiple contexts
 * @param defaultOpts - Default options applied to all contexts in namespace
 * @returns Namespace instance for managing contexts by key
 */
function createNamespace<T = any>(defaultOpts: ContextOptions): ContextNamespace;

/**
 * Get context from the default global namespace
 * @param key - Unique key identifying the context (recommend using package name)
 * @param opts - Context options, merged with namespace defaults
 * @returns Context instance for the given key
 */
function getContext<T>(key: string, opts?: ContextOptions): UseContext<T>;

/**
 * Get use function for named context from default global namespace
 * @param key - Unique key identifying the context
 * @param opts - Context options
 * @returns Function that returns current context value when called
 */
function useContext<T>(key: string, opts?: ContextOptions): () => T;

interface ContextNamespace {
  /**
   * Get or create a context by key within the namespace
   * @param key - Unique key for the context
   * @param opts - Context options, merged with namespace defaults
   * @returns Context instance for the key
   */
  get<T>(key: string, opts?: ContextOptions): UseContext<T>;
}

/**
 * The default global namespace instance
 */
const defaultNamespace: ContextNamespace;

Basic Usage

Using Default Global Namespace

import { useContext, getContext } from "unctx";

interface UserSession {
  userId: string;
  token: string;
  permissions: string[];
}

// Get context use function (recommended for library authors)
const useUserSession = useContext<UserSession>("my-auth-lib");

// Or get full context instance
const userSessionContext = getContext<UserSession>("my-auth-lib");

// Set up context in your library initialization
userSessionContext.set({
  userId: "user-123",
  token: "jwt-token",
  permissions: ["read", "write"]
});

// Use in any module
function authenticatedFetch(url: string) {
  const session = useUserSession();
  return fetch(url, {
    headers: {
      'Authorization': `Bearer ${session.token}`
    }
  });
}

Creating Custom Namespaces

import { createNamespace } from "unctx";
import { AsyncLocalStorage } from "node:async_hooks";

// Create namespace with default async context support
const myLibNamespace = createNamespace({
  asyncContext: true,
  AsyncLocalStorage
});

interface DatabaseConnection {
  host: string;
  database: string;
  transaction?: string;
}

// Get context from custom namespace
const dbContext = myLibNamespace.get<DatabaseConnection>("database");

// All contexts in this namespace inherit async support
await dbContext.callAsync(
  { host: "localhost", database: "myapp" },
  async () => {
    const db = dbContext.use();
    await executeQuery(db);
  }
);

Version Safety

Namespaces prevent conflicts when multiple versions of a library are present:

// Library v1.0 code
import { useContext } from "unctx";
const useMyLib = useContext<ConfigV1>("my-awesome-lib");

// Library v2.0 code (different interface)
import { useContext } from "unctx";
const useMyLib = useContext<ConfigV2>("my-awesome-lib");

// Both can coexist without conflicts - they share the same context
// but are typed differently in each version

Library Integration Patterns

Library Author Pattern

// In your library (my-http-client/src/context.ts)
import { useContext, getContext } from "unctx";

interface HttpClientConfig {
  baseURL: string;
  timeout: number;
  retries: number;
}

// Export the use function for consumers
export const useHttpConfig = useContext<HttpClientConfig>("my-http-client");

// Keep the full context internal for library setup
const httpConfigContext = getContext<HttpClientConfig>("my-http-client");

export function configureHttpClient(config: HttpClientConfig) {
  httpConfigContext.set(config);
}

// In your library methods
export function makeRequest(endpoint: string) {
  const config = useHttpConfig();
  return fetch(`${config.baseURL}${endpoint}`, {
    timeout: config.timeout
  });
}

Library Consumer Pattern

// In application code
import { configureHttpClient, makeRequest } from "my-http-client";

// Configure library globally
configureHttpClient({
  baseURL: "https://api.example.com",
  timeout: 5000,
  retries: 3
});

// Use throughout application
async function fetchUserData(userId: string) {
  // Library automatically uses configured context
  return makeRequest(`/users/${userId}`);
}

Namespace Context Inheritance

import { createNamespace } from "unctx";

// Create namespace with default options
const appNamespace = createNamespace({
  asyncContext: true // All contexts get async support
});

// Contexts inherit namespace defaults
const userContext = appNamespace.get<User>("user");
const sessionContext = appNamespace.get<Session>("session");

// Both contexts have async support enabled
await userContext.callAsync(userData, async () => {
  await sessionContext.callAsync(sessionData, async () => {
    // Both contexts available across async boundaries
    const user = userContext.use();
    const session = sessionContext.use();
    
    await processUserSession(user, session);
  });
});

Context Isolation

Different namespaces provide complete isolation:

import { createNamespace } from "unctx";

const libANamespace = createNamespace();
const libBNamespace = createNamespace();

const libAContext = libANamespace.get<string>("shared-key");
const libBContext = libBNamespace.get<number>("shared-key");

// These are completely separate contexts despite same key
libAContext.set("string value");
libBContext.set(42);

console.log(libAContext.use()); // "string value"
console.log(libBContext.use()); // 42

Best Practices

Key Naming Convention

// Use package name as key to avoid conflicts
const useMyLib = useContext<Config>("@company/my-lib");

// For scoped packages, use full scope
const useScoped = useContext<Data>("@scope/package-name");

// Add version suffix if needed for breaking changes
const useMyLibV2 = useContext<ConfigV2>("my-lib-v2");

Error Handling

import { getContext } from "unctx";

const appContext = getContext<AppState>("my-app");

function safeGetAppState(): AppState | null {
  try {
    return appContext.use();
  } catch {
    // Context not available, return fallback
    return null;
  }
}

// Or use tryUse for safe access
function getAppStateWithFallback(): AppState {
  return appContext.tryUse() ?? getDefaultAppState();
}

Cleanup and Lifecycle

import { getContext } from "unctx";

const resourceContext = getContext<Resource>("my-resource");

// Application shutdown cleanup
export function cleanup() {
  const resource = resourceContext.tryUse();
  if (resource) {
    resource.cleanup();
    resourceContext.unset();
  }
}

// Test isolation
beforeEach(() => {
  resourceContext.unset();
});

Install with Tessl CLI

npx tessl i tessl/npm-unctx

docs

async-context.md

build-plugins.md

context-creation.md

index.md

namespace-management.md

tile.json