Universal, protocol-agnostic types for the Hyperttp ecosystem.
@hyperttp/types is a declaration-only package. It defines the contracts shared by the
Hyperttp runtime, protocol packages, and transports without depending on REST, gRPC, HTTP,
or any other concrete protocol.
@hyperttp/types
Universal contracts: IHyperCore, HyperSender, HyperReceiver, HyperProtocol,
SendRequest, UniversalResponse, RequestContext, HyperTransport, and registries
▲
│ module augmentation
│
Protocol packages
@hyperttp/rest, @hyperttp/grpc, ...
Protocol semantics, input/output types, and convenience methods
▲
│ use HyperTransport
│
Transport packages
Node, Bun, browser, or custom transports
Protocol-agnostic network I/O
The package has three conceptual layers:
- Core types — universal dispatch, lifecycle, plugin, retry, metrics, error, and transport contracts.
- Protocol packages — protocol-specific senders, receivers, input/output types, and
optional
IHyperCoremethods. - Transports — low-level I/O implementations. A transport executes normalized requests and returns raw responses; it does not parse protocol payloads.
npm install @hyperttp/typesRuntime applications usually install this package transitively through @hyperttp/core.
Protocol and transport packages should declare it as a peer dependency when appropriate.
IHyperCore exposes protocol-neutral dispatch and registry operations:
import type {
IHyperCore,
SendRequest,
UniversalResponse,
} from "@hyperttp/types";
async function request(core: IHyperCore) {
const response: UniversalResponse = await core.send({
protocol: "custom",
input: { value: 42 },
});
return response.data;
}The core also exposes runtime introspection methods:
await core.getProtocolName();
await core.getSenderName();
await core.getReceiverName();
await core.getTransportName();These methods are part of the universal interface and return names supplied or resolved by the runtime implementation.
Protocol packages extend ProtocolInputMap and, optionally, ProtocolMetadataMap:
// inside a protocol package
import type { RestInput } from "./rest-input.js";
import type { RestMetadata } from "./rest-metadata.js";
declare module "@hyperttp/types" {
interface ProtocolInputMap {
rest: RestInput;
}
interface ProtocolMetadataMap {
rest: RestMetadata;
}
}After the augmentation is in scope, known protocol names can infer their input type when
calling send():
core.send({
protocol: "rest",
input: {
method: "GET",
url: "/users",
},
});The generic fallback overload remains available for custom or dynamically loaded protocols.
Use InferProtocolInput<P> when a reusable helper needs to expose the same behavior.
Server protocol packages use the corresponding server maps:
import type { RestServerInput, RestServerResponse } from "./server-types.js";
declare module "@hyperttp/types" {
interface ProtocolServerRequestMap {
rest: RestServerInput;
}
interface ProtocolServerResponseMap {
rest: RestServerResponse;
}
}HyperServerListenOptions uses these maps as its defaults, so a known protocol can provide
typed server handlers without repeating generic parameters:
core.listen({
protocol: "rest",
handler(request, context) {
request.path;
request.query;
request.body;
return { status: 200, body: { ok: true } };
},
});The maps are intentionally empty in this package. Concrete request and response types belong to the protocol package that owns their semantics.
A protocol sender implements the three-phase client lifecycle:
prepare → send → parse
A protocol receiver implements the three-phase server lifecycle:
receive → handle → respond
A sender can optionally expose a dynamic method surface for protocol convenience methods:
const sender = {
protocol: "rest",
methods: {
get,
post,
put,
},
prepare(request, context) {
// normalize protocol input
},
send(prepared, transport, context) {
// execute through HyperTransport
},
parse(raw, context) {
// normalize the transport response
},
};HyperMethod and HyperMethodSurface describe this dynamic plugin boundary. The actual
method signatures remain protocol-specific and should be declared by the protocol package.
HyperTransport is deliberately protocol-neutral:
import type {
HyperTransport,
TransportRequest,
TransportResponse,
} from "@hyperttp/types";
const transport: HyperTransport = {
async execute(request: TransportRequest): Promise<TransportResponse> {
// Perform runtime-specific I/O here.
throw new Error("not implemented");
},
};TransportRequest contains normalized headers and universal transport capabilities such as:
stream— request streaming when supported;followRedirects— redirect handling when supported;maxRedirects— redirect limit when redirect handling is enabled.
Flexible protocol-level input should be normalized before it reaches the transport. For
example, a REST package may accept Headers, tuples, or a record, while the transport sees
the portable normalized record form.
Transports may implement both close() and destroy():
close()is the graceful shutdown operation and may wait for pending work;destroy()is the immediate shutdown operation.
The graceful argument of IHyperCore.destroy() is forwarded to the runtime lifecycle where
supported. Actual request draining is transport-dependent. During forced shutdown, runtimes
should prefer destroy() when it is available; during graceful shutdown, they should prefer
close().
Protocol packages extend the core without adding protocol dependencies to this package:
// @hyperttp/rest/src/types.ts
import type { IHyperCore, HyperClientOptions } from "@hyperttp/types";
import type { RestInput, RestResponse } from "./types.js";
declare module "@hyperttp/types" {
interface IHyperCore {
get<T = unknown>(url: string): Promise<RestResponse<T>>;
}
interface HyperClientOptions {
rest?: {
baseUrl?: string;
};
}
interface ProtocolInputMap {
rest: RestInput;
}
interface ProtocolServerRequestMap {
rest: RestInput;
}
}Module augmentation adds compile-time contracts only. The runtime package must provide the actual sender, receiver, and convenience method implementations.
To activate an augmentation, import the protocol package or its augmentation entry point:
import "@hyperttp/core/rest";- Define protocol-specific input, output, metadata, and server request/response types.
- Implement
HyperSender,HyperReceiver, or both. - Extend
ProtocolInputMapand any other applicable maps through module augmentation. - Add optional
IHyperCoreandHyperClientOptionsextensions. - Expose the protocol sender/receiver and ensure the runtime registers them.
- Keep protocol parsing and serialization in the protocol package, not in the transport.
The following remain outside @hyperttp/types:
- REST request and response models;
- HTTP methods and REST convenience APIs;
- fetch-specific or Node-specific transport options;
- JSON parsing options;
- protocol-specific error and metadata shapes.
They belong to their protocol or transport packages and can reach the core through module augmentation where needed.
npm install
npm run build
npm run lint
npm run format:checkThe package publishes generated declaration files from dist/ and contains no runtime code.
All declarations below are re-exported from @hyperttp/types.
HyperProtocols is an empty augmentation container for protocol namespaces such as
core.rest or core.grpc.
IHyperCore extends HyperProtocols exposes:
| Member | Type | Purpose |
|---|---|---|
config |
HyperClientOptions |
Immutable client configuration. |
getProtocolName(protocol?) |
<P extends SenderProtocol = SenderProtocol>(protocol?: P) => Promise<string> |
Resolve the registered protocol name. |
getSenderName(protocol?) |
<P extends SenderProtocol = SenderProtocol>(protocol?: P) => Promise<string> |
Resolve the sender name. |
getReceiverName(protocol?) |
<P extends SenderProtocol = SenderProtocol>(protocol?: P) => Promise<string> |
Resolve the receiver name. |
getTransportName() |
() => Promise<string> |
Resolve the transport name. |
send(request) |
Typed overloads | Dispatch a protocol request. |
getSender(protocol) |
<P>(protocol: P) => AnyHyperSender<P> | undefined |
Look up a sender. |
registerSender(sender) |
<P>(sender: AnyHyperSender<P>) => this |
Register a sender. |
getReceiver(protocol) |
<P>(protocol: P) => AnyHyperReceiver<P> | undefined |
Look up a receiver. |
registerReceiver(receiver) |
<P>(receiver: AnyHyperReceiver<P>) => this |
Register a receiver. |
getProtocol(protocol) |
<P>(protocol: P) => AnyHyperProtocol<P> | undefined |
Look up a protocol module. |
registerProtocol(protocol) |
<P>(protocol: AnyHyperProtocol<P>) => this |
Register a protocol module. |
listen(options) |
<P>(options: HyperServerListenOptions<P>) => Promise<TransportServer> |
Start a protocol server. |
use(plugin) |
(plugin: HyperPlugin) => this |
Register a plugin. |
extend(options) |
(options: Partial<HyperClientOptions>) => IHyperCore |
Create a client with merged options. |
create(options) |
(options: Partial<HyperClientOptions>) => IHyperCore |
Create a new client. |
destroy(graceful?) |
(graceful?: boolean) => Promise<void> |
Shut down the client. |
The primary send() overload is:
send<TOutput = unknown, P extends SenderProtocol = SenderProtocol>(
request: SendRequest<InferProtocolInput<P>, P>,
): Promise<UniversalResponse<TOutput>>;A generic fallback is also available:
send<TInput = unknown, TOutput = unknown, P extends SenderProtocol = SenderProtocol>(
request: SendRequest<TInput, P>,
): Promise<UniversalResponse<TOutput>>;type SenderProtocol = keyof ProtocolInputMap | (string & {});It allows known augmented protocol keys as well as arbitrary custom strings.
Empty interfaces intended for module augmentation:
declare module "@hyperttp/types" {
interface ProtocolInputMap {
custom: CustomInput;
}
interface ProtocolMetadataMap {
custom: CustomMetadata;
}
}Utility types:
type InferProtocolInput<P extends string> =
P extends keyof ProtocolInputMap ? ProtocolInputMap[P] : unknown;
type InferProtocolMetadata<P extends string> =
P extends keyof ProtocolMetadataMap
? ProtocolMetadataMap[P]
: Record<string, unknown>;Shared request lifecycle context:
| Property | Type | Description |
|---|---|---|
requestId |
string |
Unique request identifier. |
startTime |
number |
Request start timestamp. |
signal |
AbortSignal | undefined |
Cancellation signal. |
meta |
Record<string, unknown> |
Shared lifecycle metadata. |
state |
Record<string, unknown> |
Mutable state shared between phases. |
Universal request envelope:
| Property | Type | Description |
|---|---|---|
protocol |
P |
Protocol identifier. |
input |
TInput or InferProtocolInput<P> |
Protocol-specific input. |
signal |
AbortSignal | undefined |
Optional cancellation signal. |
metadata |
Readonly<InferProtocolMetadata<P>> | undefined |
Protocol metadata. |
Normalized protocol-neutral response:
| Property | Type | Description |
|---|---|---|
protocol |
SenderProtocol |
Protocol that handled the request. |
ok |
boolean |
Protocol-defined success flag. |
status |
number |
Numeric status. |
statusText |
string | undefined |
Status description. |
headers |
Readonly<Record<string, string | string[]>> |
Normalized response headers. |
url |
string | undefined |
Final response URL. |
data |
TOutput |
Normalized response data. |
metadata |
Readonly<TExtra> | undefined |
Additional response metadata. |
raw |
unknown | undefined |
Raw response before normalization. |
type HyperMethod = (...args: any[]) => unknown;
type HyperMethodSurface = Readonly<Record<string, HyperMethod>>;These types describe the dynamic method boundary used by protocol senders. Concrete protocol packages should provide the precise method signatures for their own APIs.
A sender has the following members:
| Member | Type | Description |
|---|---|---|
protocol |
P |
Protocol served by the sender. |
methods |
HyperMethodSurface | undefined |
Optional convenience-method surface. |
prepare |
(request: SendRequest<TInput, P>, ctx: RequestContext) => TPrepared |
Prepare the request. |
send |
(prepared: TPrepared, transport: HyperTransport, ctx: RequestContext) => Promise<TRaw> |
Execute through a transport. |
parse |
(raw: TRaw, ctx: RequestContext) => UniversalResponse<TOutput> | Promise<UniversalResponse<TOutput>> |
Normalize the raw response. |
type AnyHyperSender<P extends SenderProtocol = SenderProtocol> =
HyperSender<any, any, any, any, P>;Use this compatibility alias at registration and configuration boundaries when the sender has
concrete, protocol-specific lifecycle types. It retains the protocol identifier while avoiding
strict function-variance incompatibilities caused by unknown input and raw types.
interface SenderRegistry {
register<P extends SenderProtocol>(sender: HyperSender<unknown, unknown, unknown, unknown, P>): void;
get<P extends SenderProtocol>(protocol: P): HyperSender<unknown, unknown, unknown, unknown, P> | undefined;
has(protocol: SenderProtocol): boolean;
readonly size: number;
}ProtocolServerRequestMap and ProtocolServerResponseMap are empty augmentation points.
Their utility types resolve known entries or fall back to unknown:
type InferProtocolServerRequest<P extends string> =
P extends keyof ProtocolServerRequestMap
? ProtocolServerRequestMap[P]
: unknown;
type InferProtocolServerResponse<P extends string> =
P extends keyof ProtocolServerResponseMap
? ProtocolServerResponseMap[P]
: unknown;Extends RequestContext with transport-dependent server information:
| Property | Type |
|---|---|
connection |
Readonly<Record<string, unknown>> | undefined |
peer |
Readonly<Record<string, unknown>> | undefined |
| Property | Type |
|---|---|
protocol |
P |
host |
string | undefined |
port |
number | undefined |
signal |
AbortSignal | undefined |
handler |
(request: TRequest, ctx: ServerRequestContext) => TResponse | Promise<TResponse> | undefined |
TRequest and TResponse default to the corresponding server protocol map entries.
| Member | Type | Description |
|---|---|---|
protocol |
P |
Protocol served by the receiver. |
receive |
(request: TRawRequest, ctx: ServerRequestContext) => TRequest | Promise<TRequest> |
Parse raw transport input. |
handle |
(request: TRequest, ctx: ServerRequestContext) => TResponse | Promise<TResponse> |
Run application logic. |
respond |
(response: TResponse, ctx: ServerRequestContext) => TRawResponse | Promise<TRawResponse> |
Serialize the protocol response. |
type AnyHyperReceiver<P extends SenderProtocol = SenderProtocol> =
HyperReceiver<any, any, any, any, P>;IHyperCore.registerReceiver() and BaseHyperClientOptions.receivers use this alias to accept
receivers with concrete protocol request and response types.
ReceiverRegistry mirrors SenderRegistry with register, get, has, and readonly size.
A protocol module contains:
protocol: P;- an optional display
name; - a
sender, areceiver, or both.
The generic module types connect the client sender and server receiver to the same protocol identifier.
type AnyHyperProtocol<P extends SenderProtocol = SenderProtocol> =
HyperProtocol<any, any, any, any, P>;This compatibility alias is used by IHyperCore registration and lookup methods,
BaseHyperClientOptions.protocols, and ProtocolRegistry. ProtocolRegistry provides
register, get, has, and readonly size methods.
type LogLevel = "debug" | "info" | "warn" | "error";| Option | Type | Description |
|---|---|---|
protocols |
AnyHyperProtocol[] | undefined |
Protocol modules to register. |
senders |
AnyHyperSender[] | undefined |
Senders to register. |
receivers |
AnyHyperReceiver[] | undefined |
Receivers to register. |
customTransport |
HyperTransport | undefined |
Custom low-level transport. |
customSender |
AnyHyperSender | undefined |
Custom sender. |
retry |
Partial<RetryOptions> | undefined |
Retry configuration overrides. |
logger |
(level: LogLevel, message: string, meta?: unknown) => void |
Custom logger. |
verbose |
boolean | undefined |
Enable verbose logging. |
pluginDirs |
string[] | undefined |
Plugin directories. |
plugins |
(HyperPlugin | string)[] | undefined |
Plugin instances or module paths. |
trackMetrics |
boolean | undefined |
Enable metrics collection. |
HyperClientOptions currently extends BaseHyperClientOptions and is an augmentation point for
protocol-specific configuration.
type PluginPhase = "START" | "PREPARE" | "CONTROL" | "FORMAT" | "NETWORK" | "DATA";
type PluginExecutionMode = "blocking" | "background";interface PluginContext {
readonly config: HyperClientOptions;
readonly core: IHyperCore;
}type PluginOnRequestResult<TInput = unknown, TOutput = unknown> =
| UniversalResponse<TOutput>
| SendRequest<TInput>
| void;A plugin has name, optional phase and mode, and optional hooks:
| Hook | Signature |
|---|---|
enabled |
(config: HyperClientOptions) => boolean |
setup |
(ctx: PluginContext) => void |
onRequest |
(req, ctx?, reqCtx?) => Promise<PluginOnRequestResult<TInput, TOutput>> | PluginOnRequestResult<TInput, TOutput> |
onResponse |
(res, req?, ctx?, reqCtx?) => Promise<UniversalResponse<TOutput> | void> | UniversalResponse<TOutput> | void |
onError |
(err, req?, ctx?, reqCtx?) => Promise<UniversalResponse<TOutput> | void> | UniversalResponse<TOutput> | void |
reqCtx is the per-request RequestContext, allowing plugins to share state and meta
across lifecycle phases.
| Option | Type |
|---|---|
maxRetries |
number | undefined |
baseDelay |
number | undefined |
maxDelay |
number | undefined |
jitter |
boolean | undefined |
Extends Error with:
| Property | Type |
|---|---|
code |
string |
cause |
unknown | undefined |
request |
TReq | undefined |
response |
TRes | undefined |
status |
number | undefined |
statusCode |
number | undefined |
meta |
{ retryCount: number; isRetryable: boolean; duration?: number; timings?: { start: number; end?: number } } | undefined |
Optional built-in counters include inflightRequests, cacheSize, queuedRequests,
activeQueue, and rateLimitHits. The interface also has a string index signature for
custom metrics: [key: string]: unknown.
Required fields:
startTime,endTime, andduration— request timing in milliseconds/timestamps;bytesReceivedandbytesSent;retries;cached.
Optional fields are statusCode, url, method, bodyHash, plugins, and stages.
stages may contain serializationMs, networkMs, and parsingMs.
interface HyperAdapter<T> {
readonly name: string;
readonly version?: string;
adapt(core: IHyperCore): T;
}Adapters translate IHyperCore into a third-party client shape such as Axios, Ky, or Got.
| Property | Type |
|---|---|
method |
string |
url |
string |
headers |
Readonly<Record<string, string | string[]>> |
body |
unknown | undefined |
signal |
AbortSignal | undefined |
protocol |
SenderProtocol |
stream |
boolean | undefined |
followRedirects |
boolean | undefined |
maxRedirects |
number | undefined |
Headers are already normalized at this layer. Protocol packages may accept more flexible
header input and normalize it before creating a TransportRequest.
| Property | Type |
|---|---|
status |
number |
statusText |
string | undefined |
headers |
Readonly<Record<string, string | string[]>> |
url |
string | undefined |
body |
unknown |
| Property | Type |
|---|---|
host |
string | undefined |
port |
number | undefined |
signal |
AbortSignal | undefined |
onRequest |
(request: TransportRequest) => Promise<TransportResponse> | TransportResponse | undefined |
onError is intentionally not part of this universal contract; transport-specific error
callbacks belong in the concrete transport package.
interface TransportServer {
close(): Promise<void> | void;
}| Member | Type |
|---|---|
protocols |
readonly SenderProtocol[] | undefined |
supports |
(protocol: SenderProtocol) => boolean (optional) |
execute |
(request: TransportRequest) => Promise<TransportResponse> |
listen |
(options: TransportListenOptions) => Promise<TransportServer> (optional) |
close |
() => Promise<void> | void (optional) |
destroy |
() => Promise<void> | void (optional) |
npm install
npm run build
npm run lint
npm run format:checkThe package publishes generated declaration files from dist/ and contains no runtime code.
MIT