# @api.global/typedrequest

> A TypeScript library for typed API requests, routing, middleware, authority guards, hooks, and unified VirtualStreams.

Latest version **8.0.4** (published 2026-09-07) · MIT license · 0 weekly downloads

## Install

```sh
npm install @api.global/typedrequest
pnpm add @api.global/typedrequest
yarn add @api.global/typedrequest
bun add @api.global/typedrequest
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 8.0.4 |
| Published | 2026-09-07 |
| First published | 2023-08-04 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 7 |
| Unpacked size | 2.3 MB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Author | Task Venture Capital GmbH |
| Maintainers | lossless |
| Keywords | TypeScript, API Requests, HTTP, Typed Programming, Request Routing, Response Handling, Virtual Streams, Asynchronous, Network Communication, Server Communication, API Integration |

## Links

- npm: https://www.npmjs.com/package/@api.global/typedrequest
- npm.io page: https://npm.io/package/@api.global/typedrequest

## Dependencies (7)

- [@push.rocks/lik](https://npm.io/package/@push.rocks/lik.md) ^6.4.2
- [@push.rocks/isounique](https://npm.io/package/@push.rocks/isounique.md) ^1.0.5
- [@push.rocks/smartdelay](https://npm.io/package/@push.rocks/smartdelay.md) ^3.1.0
- [@push.rocks/smartguard](https://npm.io/package/@push.rocks/smartguard.md) ^3.1.2
- [@push.rocks/webrequest](https://npm.io/package/@push.rocks/webrequest.md) ^4.3.0
- [@push.rocks/smartbuffer](https://npm.io/package/@push.rocks/smartbuffer.md) ^3.0.6
- [@api.global/typedrequest-interfaces](https://npm.io/package/@api.global/typedrequest-interfaces.md) ^7.1.0

## Alternatives

- [launchdarkly-js-client-sdk](https://npm.io/package/launchdarkly-js-client-sdk.md) — 2.5M weekly downloads
- [@elastic/elasticsearch](https://npm.io/package/@elastic/elasticsearch.md) — 2.1M weekly downloads
- [@c8y/client](https://npm.io/package/@c8y/client.md) — 15.3K weekly downloads
- [@signaldb/maverickjs](https://npm.io/package/@signaldb/maverickjs.md) — 1.7K weekly downloads
- [@bbc/http-transport-cache](https://npm.io/package/@bbc/http-transport-cache.md) — 1.2K weekly downloads

## Recent versions

- 8.0.4 (latest) — 2026-09-07
- 8.0.3 — 2026-08-29
- 8.0.2 — 2026-08-15
- 8.0.1 — 2026-08-15
- 8.0.0 — 2026-08-15
- 7.0.2 — 2026-08-10
- 7.0.1 — 2026-08-05
- 7.0.0 — 2026-08-04
- 6.0.0 — 2026-08-04
- 5.2.1 — 2026-08-01
- 5.2.0 — 2026-08-01
- 5.1.0 — 2026-08-01
- 5.0.0 — 2026-07-31
- 3.8.0 — 2026-07-31
- 3.7.0 — 2026-07-29
- … 49 more at https://npm.io/package/@api.global/typedrequest/versions

## README

# @api.global/typedrequest

A TypeScript library for making **fully typed request/response cycles** across any transport — HTTP, WebSockets, broadcast channels, or custom protocols. Define your API contract once as a TypeScript interface, then use it on both client and server with compile-time safety, automatic routing, middleware chains, authority guards, unified VirtualStreams, and built-in traffic monitoring hooks.

## Issue Reporting and Security

For reporting bugs, issues, or security vulnerabilities, please visit [community.foss.global/](https://community.foss.global/). This is the central community hub for all issue reporting. Developers who sign and comply with our contribution agreement and go through identification can also get a [code.foss.global/](https://code.foss.global/) account to submit Pull Requests directly.

## Install

```bash
pnpm install @api.global/typedrequest
```

You'll also want the interfaces package for defining stream types:

```bash
pnpm install @api.global/typedrequest-interfaces
```

## Usage

All examples use ESM imports and TypeScript.

```typescript
import {
  TypedRequest,
  TypedHandler,
  TypedRouter,
  TypedTarget,
  VirtualStream,
  TypedResponseError,
} from '@api.global/typedrequest';
```

### 🔑 Define Your API Contract

Every request/response pair is described by a simple interface extending `ITypedRequest`:

```typescript
import type { ITypedRequest } from '@api.global/typedrequest-interfaces';

interface IGetUser extends ITypedRequest {
  method: 'getUser';
  request: { userId: string };
  response: { username: string; email: string };
}

interface IAddNumbers extends ITypedRequest {
  method: 'add';
  request: { a: number; b: number };
  response: { result: number };
}
```

The `method` field acts as a discriminator — it's what the router uses to match requests to handlers.

### 📡 Making Typed Requests (Client Side)

`TypedRequest` fires a typed request against an HTTP endpoint or a `TypedTarget`:

```typescript
// Against an HTTP endpoint
const getUser = new TypedRequest<IGetUser>('https://api.example.com/rpc', 'getUser');
const user = await getUser.fire({ userId: 'user-123' });
console.log(user.username); // fully typed!

// With response caching
const cachedUser = await getUser.fire({ userId: 'user-123' }, true);

// With an end-to-end HTTP deadline
const boundedUser = await getUser.fire(
  { userId: 'user-123' },
  {
    timeoutMs: 5_000,
    abortSignal: navigationAbortController.signal,
    useCache: false,
    maxRetries: 3,
  }
);
```

The boolean second argument remains supported. The options form accepts:

- `useCache?: boolean` — `false` uses the network only; `true` uses Webrequest's persistent HTTP cache under keys isolated by endpoint, RPC method, and request body. Webrequest enforces `Cache-Control`, `no-store`, freshness, conditional revalidation, and its 256-entry default bound. Cache hits are accepted only for successful non-stream response payloads and are placed in a new envelope for the current request identity; cached identity and correlation fields are never trusted.
- `timeoutMs?: number` — a positive integer up to `2_147_483_647`. One monotonic deadline is checked during request preparation and bounds HTTP transport, JSON body consumption, server-directed retries, and retry waits. Synchronous code cannot be interrupted between checks.
- `maxRetries?: number` — a safe integer from `0` to `1_000`; defaults to `3`. This bounds server-directed retry responses even when no request deadline is configured.
- `abortSignal?: AbortSignal` — requests cancellation at preparation checkpoints and is passed to transport, retry waits, response decoding, and VirtualStream staging. Custom targets must declare `supportsAbortSignal: true` and cooperate with the signal; TypedRequest cannot forcibly stop synchronous or non-cooperative work.

Deadline failures throw `TypedRequestTimeoutError`, which exposes the RPC
`method` and original `timeoutMs`. An HTTP abort or timeout is never converted
into a stale-cache fallback. A `TypedTarget` can opt into the same contract by
declaring `supportsAbortSignal: true` and settling its transport work with
`options.signal.reason` when the signal aborts. Deadlines are rejected for custom
targets that do not declare that support.

Exhausting `maxRetries` throws `TypedRequestRetryLimitError`, which exposes the
RPC `method` and configured `maxRetries`.

VirtualStream traffic after the parent request is owned by the parent
transport's bounded endpoint lifecycle rather than the initial request deadline.

### 🛠️ Handling Requests (Server Side)

`TypedHandler` processes a specific method and returns a typed response:

```typescript
const addHandler = new TypedHandler<IAddNumbers>('add', async (req) => {
  return { result: req.a + req.b };
});
```

The second argument to the handler function is an optional `TypedTools` instance that gives access to guard validation and transport-layer context:

```typescript
const secureHandler = new TypedHandler<IGetUser>('getUser', async (req, tools) => {
  // Access transport-layer context (e.g., authenticated user info)
  const peer = tools.localData.peer;
  tools.abortSignal.throwIfAborted();

  // Validate with guards
  await tools.passGuards([myAuthGuard], req);

  return { username: 'Alice', email: 'alice@example.com' };
});
```

`tools.abortSignal` is transport-owned and never derived from `localData` or
other wire-controlled fields. A local call or a transport without remote
cancellation receives a non-aborted fallback signal; handlers must observe the
signal themselves, and must not rely on fallback signal object identity.

### 🚦 Routing Requests

`TypedRouter` dispatches incoming requests to the correct handler based on the `method` field:

```typescript
const router = new TypedRouter();
router.addTypedHandler(addHandler);
router.addTypedHandler(secureHandler);

// Route an incoming request object
const response = await router.routeAndAddResponse(incomingTypedRequest);
```

When a transport supplies authenticated connection or request context, pass it
as `trustedLocalData`. Before authority checks or stream graph decoding, the
router deletes configurable own `localData` properties without reading them and
shadows inherited properties with a non-enumerable `undefined` data property.
An envelope is dropped before any authority callback if an own property cannot
be removed or an inherited property cannot be safely masked. Authority contexts
receive a frozen `trustedLocalData` copy, and handlers receive a separate copy
only after decoding succeeds:

```typescript
const response = await router.routeAndAddResponse(incomingTypedRequest, {
  trustedLocalData: {
    requestContext,
    authenticatedUser,
  },
});
```

Handlers receive these server-owned values through `tools.localData`. Use this
option at network trust boundaries instead of copying `localData` from the
request.

Incoming request guards, incoming response guards, and incoming request signal
resolvers receive the readonly `wireLocalDataPresent` boolean in their authority
context. It is captured before sanitization and is `true` only when the raw
envelope had its own enumerable `localData` property, matching the property
shape serialized by ordinary object transports. Inherited and non-enumerable
properties do not count. No raw `localData` value is exposed at any of these
boundaries, and neither route options nor `localRequest` can set or suppress the
captured flag.

Cancellation-aware transports register the trusted signal separately and own
its terminal cleanup:

```typescript
const detachSignalResolver = router.registerIncomingRequestSignalResolver(
  (request, context) => cancellationRegistry.register({ request, context }),
);
```

The resolver returns `{ abortSignal, complete }`. Once a registration is
returned, the router attempts its `complete()` callback once when that routing
attempt terminates, including missing handlers, middleware denial, decode
failure, and handler failure. Callback errors are contained. A transport that
routes a request directly may instead pass `trustedAbortSignal` to
`routeAndAddResponse()`. Neither channel is serialized into `localData`.
The resolver's authority context contains the same `wireLocalDataPresent` and
frozen `trustedLocalData` values seen by the incoming request guard.
Requests entering a registered signal resolver must carry a non-empty
transport-generated `requestInstanceId`; malformed identities are rejected
before resolver or handler invocation. Local routing without a resolver remains
available for request objects that do not carry this transport identity.

A transport should install one synchronous incoming request authority guard on
each application entry router:

```typescript
const detachRequestGuard = router.registerIncomingRequestAuthorityGuard(
  (request, context) =>
    context.typedRouter === router &&
    !context.wireLocalDataPresent &&
    context.trustedLocalData.peer === exactPeer &&
    request.correlation?.phase === 'request'
);
```

The guard must return literal `true`. False, truthy non-booleans, and thrown
results deny the request with `null`. Denial occurs before stream transport
resolution, descriptor validation or staging, hooks, handler lookup,
middleware, and handlers. `localRequest` does not bypass this boundary, and a
guard on an application router also gates handlers reached through fallback
routers. Only one owner may register a request guard; its identity-safe detach
function is idempotent.

Routers are **composable** — you can nest them to build modular API architectures:

```typescript
const coreRouter = new TypedRouter();
const authRouter = new TypedRouter();

// Each sub-router manages its own handlers
coreRouter.addTypedHandler(addHandler);
authRouter.addTypedHandler(secureHandler);

// Link them together — requests flow through the entire chain
const mainRouter = new TypedRouter();
const detachCoreRouter = mainRouter.addTypedRouter(coreRouter);
const detachAuthRouter = mainRouter.addTypedRouter(authRouter);

function shutdown() {
  detachCoreRouter();
  detachAuthRouter();
}
```

`addTypedRouter()` returns an idempotent disposer for the bidirectional edge
created by that call. Adding an already-connected router returns a no-op
disposer, so it cannot detach another call's edge. Disposal removes the child
from both routers, permits a later re-add, and affects future handler lookups;
a request that already selected its handler continues to completion.

Use a one-way fallback when several isolated application routers need access
to the same protocol handlers without gaining access to each other:

```typescript
const publicRouter = new TypedRouter();
const internalProtocolRouter = new TypedRouter();

internalProtocolRouter.addTypedHandler(protocolHandler);
const detachProtocolRouter = publicRouter.addFallbackRouter(internalProtocolRouter);

function shutdown() {
  // Release the owned composition edge after the router's active lifetime.
  detachProtocolRouter();
}
```

Requests entering `publicRouter` can reach `protocolHandler`; requests entering
`internalProtocolRouter` cannot reach public handlers. TypedRouter rejects
method collisions both when routers are composed and when handlers are added
later, and rejects fallback edges that would create a routing cycle. The
returned idempotent disposer removes only an edge created by that call; adding
an already-connected fallback returns a no-op disposer.

Use `router.getMethodNames()` to inspect the sorted set of method names
reachable through the router's local, bidirectional, and fallback composition.

### 🛡️ Middleware

Add middleware functions to a `TypedRouter` that run **before** any handler on that router executes. Middleware is great for authentication, logging, rate limiting, or input validation:

```typescript
const router = new TypedRouter();

// Add authentication middleware
router.addMiddleware(async (typedRequest) => {
  const token = typedRequest.localData?.authToken;
  if (!token || !isValidToken(token)) {
    throw new TypedResponseError('Unauthorized', { reason: 'invalid_token' });
  }
});

// Add logging middleware
router.addMiddleware(async (typedRequest) => {
  console.log(`Processing ${typedRequest.method}`);
});

// Handlers are only reached if all middleware passes
router.addTypedHandler(secureHandler);
```

Middleware functions receive the full `ITypedRequest` object and run in the order they were added. Throw a `TypedResponseError` from any middleware to reject the request before it reaches the handler.

### 🎯 Custom Targets with TypedTarget

For non-HTTP transports (WebSockets, broadcast channels, IPC), use `TypedTarget` with a custom post function:

```typescript
// Synchronous target (post returns the response directly)
const target = new TypedTarget({
  postMethod: async (payload) => {
    // Send via your custom transport and return the response
    return await myWebSocket.sendAndWait(payload);
  },
});

const request = new TypedRequest<IGetUser>(target, 'getUser');
const user = await request.fire({ userId: 'user-123' });
```

Custom targets that support request deadlines or VirtualStreams must declare
and honor cancellation:

```typescript
const boundedTarget = new TypedTarget({
  supportsAbortSignal: true,
  postMethod: async (payload, options) => {
    return myTransport.sendAndWait(payload, {
      signal: options?.signal,
    });
  },
});
```

Every `fire()` creates a fresh top-level `requestInstanceId`, including a fresh
ID for each server-directed retry. Network and custom-target responses must echo
the exact ID. Async response interests and transport cancellation registries
must key the exact method, correlation ID, and request instance ID so a stale
response or cancel cannot affect a later attempt that reuses the correlation ID.

For **async targets** where the response arrives separately (e.g., WebSocket push), pair a `TypedTarget` with a `TypedRouter`:

```typescript
const router = new TypedRouter();
const exactPeer = getTransportPeer();

const detachResponseGuard = router.registerIncomingResponseAuthorityGuard(
  (response, context) =>
    context.typedRouter === router &&
    !context.wireLocalDataPresent &&
    context.trustedLocalData.peer === exactPeer
);

const asyncTarget = new TypedTarget({
  postMethodWithTypedRouter: async (payload) => {
    // Fire-and-forget — response will arrive via router
    mySocket.send(JSON.stringify(payload));
  },
  typedRouterRef: router,
});

// When the response arrives later, route it back:
mySocket.onMessage((data) => {
  router.routeAndAddResponse(JSON.parse(data), {
    trustedLocalData: { peer: exactPeer },
  });
});

// During transport shutdown:
detachResponseGuard();
```

Separately routed responses are bounded by `responseTimeoutMs`, which defaults
to 30 seconds. Expired and aborted waits remove their router interest
immediately. A transport should install one response authority guard and bind
each response to its exact peer or session authority. A guard must return
literal `true`; false and thrown results drop the response before hooks and
correlation-interest fulfillment. Only one guard owner may be registered, and
its detach function is idempotent. Use `context.wireLocalDataPresent` when the
transport protocol forbids `localData` on incoming response envelopes.

### Virtual Streams

`VirtualStream` is one transport-neutral facade for finite transfers and
open-ended streams such as camera frames. It carries ordered logical
`Uint8Array` chunks through an exact `virtual-stream-v1` endpoint. One
`send()` or `WritableStream.write()` maps to one `receive()` result or readable
enqueue even when the parent transport fragments data internally.

Shared DTOs use `TVirtualStream<'send'>` or `TVirtualStream<'receive'>`. The
direction is always local to the requester. `TypedHandler` applies
`TReverseVirtualStreamDirections` recursively to objects, tuples, and function
signatures, so a request stream declared as requester-local `send` reaches the
handler as local `receive`. A response declared as requester-local `send` is
created by the handler as local `receive` and arrives at the requester as local
`send`.

Non-stream values keep their exact types, including branded primitives and
overloaded or generic callbacks. Direction mapping preserves the codec's opaque
values (dates, regular expressions, binary views, maps, sets and promises).
Async iterable resource handles, including native sockets, are also opaque. Recursive
DTO graphs use exact type identity to avoid revisiting a cycle; structurally
assignable descendants still receive direction mapping.

```typescript
import type {
  ITypedRequest,
  TVirtualStream,
} from '@api.global/typedrequest-interfaces';

interface IUploadRequest extends ITypedRequest {
  method: 'upload';
  request: { stream: TVirtualStream<'send'> };
  response: { stored: boolean };
}
```

A transport creates an explicit, already-authorized creator registration
through its own high-level stream factory. Bind that exact registration to the
public facade:

```typescript
import type {
  IVirtualStreamTransport,
  IVirtualStreamTransportRegistration,
} from '@api.global/typedrequest-interfaces';

declare const virtualStreamTransport: IVirtualStreamTransport;
declare const registration: IVirtualStreamTransportRegistration<'send'>;

const sendingStream = VirtualStream.fromRegistration({
  transport: virtualStreamTransport,
  registration,
});
```

`VirtualStream` has no public empty constructor and never registers a stream
during encoding. The registration direction, endpoint direction, stream ID,
content type, and integrity must match exactly. Encoding emits the registration
descriptor directly:

```typescript
{
  _isVirtualStream: true,
  protocol: 'virtual-stream-v1',
  streamId: 'transport-scoped-id',
  creatorDirection: 'send',
  contentType: 'application/octet-stream', // optional
  integrity: {                             // optional for open-ended streams
    algorithm: 'sha256',
    byteLength: 12345,
    digest: `sha256:${'0'.repeat(64)}`,
  },
  transport: { opaqueTransportData: true },
}
```

The `transport` value is JSON-compatible and interpreted only by the parent
transport. TypedRequest validates its shape but does not normalize or compare
it. Repeated stream IDs in one graph deduplicate to one facade and one staged
open when creator direction, content type, and integrity match; the first
transport payload is authoritative. Streams beneath the echoed `.request`
inside a response envelope are omitted so they are not opened twice.

All facades expose `protocol`, local `direction`, `streamId`, optional
`contentType` and `integrity`, `opened`, `completion`, `closed`, and `abort()`.
Senders add `send()`, `writable`, and `close()`. Receivers add `receive()`,
`readable`, `accept()`, and `reject()`. `undefined` from `receive()` is graceful
EOF. A finite receiver drains EOF and calls `accept()`; an open-ended stream
omits `integrity`. `completion` resolves with the same accepted receipt on both
peers and rejects for every abnormal outcome.

Decoded endpoints are transport-staged and application-blocked. The descriptor
consumer alone may initiate physical OPEN, and only after TypedRequest reaches
an established application boundary. On incoming requests, the router runs the
owning middleware chain while every stream operation remains blocked, then
authorizes request facades immediately before invoking the handler. On
successful responses, TypedRequest authorizes decoded facades only after
response authority, envelope, protocol-error, and descriptor validation. The
transport's `openVirtualStream()` must return a staged opposite-direction
endpoint without control or data activity. Its second argument contains the
remaining `timeoutMs` and an `abortSignal`; transports must settle staging
promptly with the signal's exact reason when it aborts.

Published or opened endpoints are aborted on parent failure, timeout,
middleware denial, handler error, partial staging failure, or response encoding
failure. An unpublished, unused explicit registration is rolled back only with
its synchronous no-I/O disposer. Published registration disposal waits for the
endpoint's mandatory `closed` settlement. Transports must always resolve
`closed` within a bounded terminal-cleanup deadline, including when a terminal
operation rejects. Terminal `close`, `accept`, `reject`, and `abort` operations
are serialized so cleanup and disposer ownership cannot race.

Custom targets carrying streams must supply the exact creator transport and
support parent cancellation:

```typescript
const target = new TypedTarget({
  virtualStreamTransport,
  supportsAbortSignal: true,
  postMethod: sendRequest,
});
```

Response caching is unavailable for requests containing stream descriptors.
The initial request deadline also bounds response stream staging. Every
`openVirtualStream()` call is capped at 10 seconds by the package, and
`decodePayloadFromNetworkAsync()` accepts an absolute `openDeadlineAt` override
based on `performance.now()`. All descriptors in one graph share that deadline;
each sequential transport call receives only the remaining budget.
The stream transport owns the subsequent physical OPEN and terminal lifecycle.
Creator operations may wait boundedly for the descriptor consumer. A staged
endpoint that resolves after its timeout is aborted and held through `closed`
rather than orphaned.

On a server, resolve the exact per-peer transport from trusted route-local data:

```typescript
const detachVirtualStreamResolver =
  router.registerVirtualStreamTransportResolver(
    (trustedLocalData) =>
      trustedLocalData.virtualStreamTransport as
        | IVirtualStreamTransport
        | undefined
  );

const response = await router.routeAndAddResponse(incomingRequest, {
  trustedLocalData: {
    virtualStreamTransport: peer.virtualStreamTransport,
  },
});

detachVirtualStreamResolver();
```

Only one resolver owner can be registered. Its identity-safe disposer is
idempotent. Resolver selection sees only frozen trusted transport data and runs
only after the incoming request authority guard succeeds. The resolved
transport is reused for request decoding and response encoding on that route.

Decoding is asynchronous and graph-first. Use
`await VirtualStream.decodePayloadFromNetworkAsync(payload, commFunctions)`
outside `TypedRequest` or `TypedRouter`. Standalone decoding authorizes before
returning. Framework callers pass a `VirtualStreamActivationScope`, stage every
descriptor only after complete graph validation and deduplication, and call
`scope.authorizeApplicationUse()` at their established application boundary.

### ⚠️ Error Handling

Throw `TypedResponseError` inside handlers to send structured errors back to the caller:

```typescript
const handler = new TypedHandler<IGetUser>('getUser', async (req) => {
  const user = await db.findUser(req.userId);
  if (!user) {
    throw new TypedResponseError('User not found', { userId: req.userId });
  }
  return { username: user.name, email: user.email };
});
```

On the client side, `TypedResponseError` is thrown when the server responds with an error:

```typescript
try {
  await getUser.fire({ userId: 'nonexistent' });
} catch (err) {
  if (err instanceof TypedResponseError) {
    console.error(err.errorText); // 'User not found'
    console.error(err.errorData); // { userId: 'nonexistent' }
  }
}
```

### 📊 Traffic Monitoring Hooks

Hooks are metadata-only by default. Every runtime entry includes correlation,
method, direction, phase, timing, and `hasError`, while `payload` is
`TYPED_REQUEST_REDACTED_PAYLOAD`, `payloadRedacted` is `true`, and `error` is
omitted. Treat `payloadRedacted` as authoritative; sentinel object identity is
not stable across serialization or independently loaded bundles. For source
compatibility, `ITypedRequestLogEntry` remains mutable with `payload: any`, and
the two new flags are optional so pre-3.8 constructed entries still compile.
Entries emitted to callbacks always contain both flags and are frozen at
runtime.

```typescript
TypedRouter.setGlobalHooks({
  onOutgoingRequest: (entry) => {
    console.log(`${entry.method} [${entry.correlationId}]`);
  },
  onIncomingResponse: async (entry) => {
    await metrics.record({
      method: entry.method,
      durationMs: entry.durationMs,
      failed: entry.hasError,
    });
  },
});
```

Payload or error access requires explicit projector options. Projectors are a
trusted boundary: they synchronously receive the live raw value before the
hook callback receives an independent `structuredClone` snapshot with a
top-level freeze. Project only an allowlist rather than returning the complete
payload. Projections containing `SharedArrayBuffer`, including nested typed
arrays or `DataView` instances backed by one, fail closed to the redacted
sentinel because cloning them would retain shared mutable memory.

```typescript
TypedRouter.setGlobalHooks(
  {
    onIncomingRequest: (entry) => metrics.trackRequest(entry),
    onOutgoingResponse: (entry) => metrics.trackResponse(entry),
  },
  {
    payloadProjector: (payload, metadata) => ({
      phase: metadata.phase,
      tenantId: (payload as { tenantId?: string }).tenantId,
    }),
    errorProjector: (_errorText, metadata) =>
      metadata.hasError ? 'request failed' : undefined,
  }
);
```

The `globalHooks` getter/setter and `setGlobalHooks()` remain available.
Assignments and phases updated without projector options use metadata-only
redaction; projector options from an earlier callback are never inherited by a
replacement callback. Assignment preserves exact object identity, so
`TypedRouter.globalHooks === hooks` and `router.hooks === hooks` after their
respective assignments. Direct callback replacement invalidates projector
options unless that exact callback received them. Per-router hooks accept the
same optional projector argument through
`router.setHooks(hooks, projectionOptions)`.

Use `addGlobalHooks()` when independently owned subscribers need the same
phase. Each subscriber receives its own projection and snapshot. The returned
disposer is idempotent.

```typescript
const disposeAuditHooks = TypedRouter.addGlobalHooks(
  {
    onIncomingRequest: (entry) => auditQueue.push(entry),
  },
  {
    payloadProjector: (payload) => ({
      tenantId: (payload as { tenantId?: string }).tenantId,
    }),
  }
);

disposeAuditHooks();
```

Hook, projector, clone, and freeze failures are isolated from routing and from
other subscribers. Recipients, callbacks, and projector options are snapshotted
at dispatch start, so adding, disposing, clearing, or replacing hooks during a
callback affects only future entries. Set `request.skipHooks = true` for a
client request, or pass `{ skipHooks: true }` to
`router.routeAndAddResponse()` when routing an internal message that must not
emit hooks.

### 🏗️ Architecture Overview

```
┌─────────────┐       ┌──────────────┐       ┌──────────────┐
│ TypedRequest │──────▶│ HTTP / WS /  │──────▶│ TypedRouter  │
│  (client)    │       │ TypedTarget  │       │  (server)    │
└─────────────┘       └──────────────┘       └──────┬───────┘
                                                     │
                                              ┌──────▼───────┐
                                              │  Middleware   │
                                              └──────┬───────┘
                                                     │
                                              ┌──────▼───────┐
                                              │ TypedHandler  │
                                              │ (your logic)  │
                                              └──────────────┘
```

| Component | Role |
|---|---|
| **TypedRequest** | Fires typed requests against a URL or TypedTarget |
| **TypedTarget** | Abstracts the transport layer (HTTP, WebSocket, custom) |
| **TypedRouter** | Routes incoming requests to the correct handler; supports bidirectional `addTypedRouter()`, one-way `addFallbackRouter()`, and sorted reachable-method introspection through `getMethodNames()` |
| **TypedHandler** | Processes a single method and returns a typed response |
| **Middleware** | Pre-handler functions for auth, validation, logging — throw `TypedResponseError` to reject |
| **VirtualStream** | Direction-specific facade over an exact staged `virtual-stream-v1` transport endpoint |
| **TypedResponseError** | Structured error propagation across the wire |
| **TypedTools** | Guard validation and transport-layer context available inside handlers |

## License and Legal Information

This repository contains open-source code licensed under the MIT License. A copy of the license can be found in [license.md](./license.md).

**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.

### Trademarks

This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH or third parties, and are not included within the scope of the MIT license granted herein.

Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines or the guidelines of the respective third-party owners, and any usage must be approved in writing. Third-party trademarks used herein are the property of their respective owners and used only in a descriptive manner, e.g. for an implementation of an API or similar.

### Company Information

Task Venture Capital GmbH<br>
Registered at District Court Bremen HRB 35230 HB, Germany

For any legal inquiries or further information, please contact us via email at hello@task.vc.

By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.

---
_Source: https://npm.io/package/@api.global/typedrequest · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
