npm package: @oamm/runtime-context
Description: Shared, injectable request-scoped runtime context for Node.js and Edge runtimes
Author: Alex Mora
Latest version: 1.0.7

# @oamm/runtime-context

A production-ready TypeScript library that provides a shared, injectable request-scoped runtime context. Primarily backed by Node.js `AsyncLocalStorage`, but designed to be framework-agnostic and safe for environments where ALS is unavailable (like Edge runtimes).

## Why?

In complex applications, multiple libraries or modules might need access to request-scoped data (like trace IDs, user information, etc.). If each library creates its own `AsyncLocalStorage` instance, they won't share data, leading to a "split-brain" problem. 

This package provides a single, shared storage instance that can be injected into or shared across multiple libraries.

## Installation

```bash
npm install @oamm/runtime-context
```

## Usage

### Basic Usage (Node.js)

```typescript
import { runWithContext, getContext, setContext } from '@oamm/runtime-context';

const myContext = { requestId: '123' };

runWithContext(myContext, () => {
  const ctx = getContext(); // { requestId: '123' }
  setContext('userId', 'abc');
  // ctx is now { requestId: '123', userId: 'abc' }
});
```

### Context Management Scenarios

The library supports three distinct scenarios for managing context, giving you flexibility depending on your needs.

#### 1. Map-based Multi-Context (Automatic)
Use this when you want a clean, Map-based storage for various pieces of data. This allows you to use `setContext` and `getContext` immediately with keys.

```typescript
import { runWithContext, setContext, getContext } from '@oamm/runtime-context';

runWithContext(() => {
  setContext('requestId', '123');
  // ...
  const id = getContext('requestId'); // '123'
});
```

#### 2. Keyed Context (Multi-tenant/Shared)
Use this when you have multiple independent contexts (e.g., a Database context and a User context) and you want to keep them separated without mixing.

```typescript
import { runWithContext, getContext } from '@oamm/runtime-context';

const dbCtx = { connection: '...' };
const userCtx = { id: 'abc' };

runWithContext('db', dbCtx, () => {
  runWithContext('user', userCtx, () => {
    const db = getContext('db');     // { connection: '...' }
    const user = getContext('user'); // { id: 'abc' }
    
    // getContext() returns the whole Map containing both
  });
});
```

#### 3. Raw Context (Simple Object)
Use this for simple scenarios where you just need a single object as context and want to avoid the overhead of a `Map`.

```typescript
import { runWithContext, getContext, setContext } from '@oamm/runtime-context';

const myContext = { requestId: '123' };

runWithContext(myContext, () => {
  const ctx = getContext(); // { requestId: '123' }
  setContext('userId', 'abc'); 
  // myContext is now { requestId: '123', userId: 'abc' }
});
```

### Store Architecture

The library automatically manages the underlying storage structure based on how you initialize your context. Understanding the distinction between the **Store** and **Context Objects** is key:

*   **Store**: The top-level container held by `AsyncLocalStorage`. It is either a `Map` (Scenarios 1 & 2) or a Raw Object (Scenario 3).
*   **Context Object**: An object stored *inside* the Map (Scenario 2) or the Store itself if it's a Raw Object.

#### Visual Representation

**1. Map-based Store (Scenarios 1 & 2)**
```text
ALS Store (Map)
│
├── 'db'   ──> { connection: '...' }  (Keyed Context)
├── 'user' ──> { id: 'abc' }          (Keyed Context)
└── [Default] ──> { requestId: '123' }   (Default Fallback Context)
```

**2. Raw Object Store (Scenario 3)**
```text
ALS Store (Object)
│
└── { requestId: '123' }
```

### Shared Storage (Injection)

If you are building a library that depends on this one, you can allow users to inject their own storage to ensure consistency:

```typescript
import { initRuntimeContext } from '@oamm/runtime-context';

// In your app entry point
initRuntimeContext({ storage: mySharedStorage });
```

### Debug Mode

You can enable internal debug logging to gain visibility into context operations (context entry, retrieval, and modifications). This is useful for troubleshooting context availability or state changes.

```typescript
import { initRuntimeContext } from '@oamm/runtime-context';

initRuntimeContext({
  debug: true
});
```

When enabled, the library logs detailed information to `console.debug` prefixed with `[runtime-context]`.

### ensureContext

Ensures a context exists without double-wrapping, reusing the existing one if available. It supports the same three scenarios:

#### 1. Map-based context (Default)
Ensures that a Map-based storage is available.
```typescript
import { ensureContext, setItem } from '@oamm/runtime-context';

await ensureContext(async () => {
  setItem('traceId', 'abc');
});
```

#### 2. Keyed context
Ensures a specific key exists within a Map-based context.
```typescript
await ensureContext('my-key', () => ({ data: 1 }), () => {
  // If 'my-key' already exists, it is reused.
});
```

#### 3. Raw (object-based) context
Ensures a specific context object exists. No Map is created if it's missing.
```typescript
await ensureContext(() => ({ requestId: '123' }), async () => {
  // If a context already existed, it is reused.
  // Otherwise, a new object context is created for this scope.
});
```

### Sharing across libraries

If you are building a library that needs access to the request context, simply import `getContext` from this package. 

```typescript
// my-library.ts
import { getContext } from '@oamm/runtime-context';

export function myLibraryFunction() {
  const ctx = getContext();
  // ... do something with ctx ...
}
```

As long as the main application uses `@oamm/runtime-context`, your library will automatically have access to the same context. 

This works even if your library is bundled separately, thanks to our use of a global storage key.

## API

### Core

* `initRuntimeContext(config)`: Initializes the global storage and configuration (e.g., `debug` mode, `storage` injection).
* `getContext<T>(key?)`: Returns the current context or `undefined`. Supports automatic type inference when using classes as keys.
* `requireContext<T>(key?)`: Returns the current context or throws.
* `runWithContext(fn)`: Runs `fn` with an automatically initialized Map-based context (Scenario 1).
* `runWithContext(key, ctx, fn)`: Runs `fn` with a specific key in a Map-based multi-context (Scenario 2).
* `runWithContext(ctx, fn)`: Runs `fn` within the given context, explicitly avoiding Map creation (Scenario 3).
* `ensureContext(fn)`: Ensures a Map-based context exists (Scenario 1).
* `ensureContext(key, create, fn)`: Ensures a specific keyed context exists in a Map (Scenario 2).
* `ensureContext(create, fn)`: Ensures a context exists, avoiding Map creation if it needs to be created (Scenario 3).

### Multi-Context (Map-based)

When you need to manage multiple independent contexts (e.g., a TokenKit and a Session), you can use keys (strings, symbols, or classes). The library automatically handles Map creation and **clones the parent Map** when nesting contexts to ensure changes in a nested scope do not leak back to the parent.

```typescript
class TokenKit {
  constructor(public token: string) {}
}

const tk = new TokenKit('abc');

runWithContext(TokenKit, tk, () => {
  // Type is automatically inferred as TokenKit | undefined
  const context = getContext(TokenKit); 
});
```

#### Bound Accessors (Method References)

If you need a reference to a context accessor (e.g., for dependency injection), use an arrow function:

```typescript
// For classes:
const getSessionContext = () => getContext(SessionContext);

// For interfaces or default context (uses the whole store):
interface MyContext { user: string }
const getMyContext = () => getContext<MyContext>();

// For specific keys with interfaces:
const getSpecific = () => getContext<MyContext>('my-key');

// Later, call them without arguments
const session = getSessionContext(); // inferred as SessionContext | undefined
```

### Helpers

The library provides several helpers to interact with the context. There is an important distinction between interacting with the **Store** (Map or Object) and interacting with **Properties** within a context object.

#### Store Helpers (Top-level)

*   `setContext(key, value)`: Directly interacts with the top-level ALS store.
    *   **Map-based**: Performs `map.set(key, value)`.
    *   **Raw Object**: Performs `object[key] = value`.
*   `getContext(key?)`: Retrieves a value from the ALS store.
*   `requireContext(key?)`: Retrieves a value from the ALS store or throws an error.

#### Property Helpers (Object-level)

These helpers are designed to work with properties *inside* context objects, and they automatically handle the "Default Context Fallback".

*   `setValue(key, value, contextKey?)`: Sets a property on a context object.
    *   If `contextKey` is provided, it targets the object stored under that key in the Map.
    *   If not provided, it targets the **Default Context** object. If no default object exists, it falls back to `setContext(key, value)`.
*   `getValue(key, contextKey?)`: Gets a property from a context object.
    *   If `contextKey` is provided, it targets the object at that key.
    *   If not provided, it first checks the Map for `key`. If not found, it falls back to checking the **Default Context** object for that property.
*   `mergeContext(partial, contextKey?)`: Merges an object into a context object (targets the Default Context if no `contextKey` is provided).

#### Default Context Fallback

When transitioning from a Raw Object context (Scenario 3) to a Map-based context (Scenarios 1 & 2), the library automatically preserves the parent object as a "default" context. 

```typescript
const globalContext = { traceId: '123' };

runWithContext(globalContext, () => {
  // We are in Scenario 3 (Raw Object)
  
  runWithContext('user', { id: 'abc' }, () => {
    // We are now in Scenario 2 (Map-based)
    // The Map contains: { 'user' => { id: 'abc' }, [Default] => globalContext }
    
    const traceId = getValue('traceId'); // '123' (fallback to default context)
    const userId = getValue('id', 'user'); // 'abc' (from 'user' keyed context)
    
    setValue('newVal', 'foo'); // Sets globalContext.newVal = 'foo'
  });
});
```

### Helpers (Map-based context)

* `setItem(key, value)`
* `getItem(key)`

### Environment Support

* **Node.js**: Automatically uses `AsyncLocalStorage`.
* **Edge/Other**: You must inject an `AsyncContextStorage` implementation or use `initRuntimeContext({ storage })` if you want to share storage across libraries. By default, it will throw an error if you try to `run` without a valid storage.

## Testing

Use `resetForTests()` in `beforeEach` to ensure test isolation.

```typescript
import { resetForTests } from '@oamm/runtime-context';

beforeEach(() => {
  resetForTests();
});
```