Public TypeScript API

Documentation for Redweb 0.13.3. Install that exact version when following these examples.

index.d.ts

ts
declare module 'redweb' {
    export { defineSocketContract } from 'redweb/contract';
    export type { SocketContract, ContractClient, SocketSchema, ContractInput, ContractOutput, ContractMessage } from 'redweb/contract';
    import { Application, RequestHandler } from 'express';
    import { CorsOptions } from 'cors';
    import { Server as NodeHttpServer } from 'http';
    import { Server as NodeHttpsServer } from 'https';
    import { WebSocket, ServerOptions } from 'ws';
    import { Buffer } from 'buffer';
    import { EventEmitter } from 'events';

    /** ─────────────────── HTTP / CORE ─────────────────── */

    export type RedWebEncoding = 'json' | 'urlencoded';

    export interface RedWebOptions {
        port?: number;
        bind?: string;
        publicPaths?: string[];
        services?: Array<{ serviceName: string; method: string; function: RequestHandler }>;
        listen?: boolean;
        listenCallback?: () => void;
        encoding?: RedWebEncoding;
        ssl?: { key: string; cert: string };
        server?: Application;
        corsOptions?: CorsOptions | false;
        exposeErrors?: boolean;
        logger?: RedWebLogger | null;
    }

    export type RedWebSocket = WebSocket & {
        clientKey: string;
        __redwebClientKey: string;
        remoteAddress: string;
        isAssigned: boolean;
        sendJson(data: unknown): boolean;
        broadcast(data: unknown): number;
        context?: RedWebConnectionContext;
        joinRoom?(roomId: string): boolean;
        /** Bounded permission check followed by atomic membership insertion. */
        enterRoom?(roomId: string): Promise<boolean>;
        leaveRoom?(roomId: string): boolean;
        roomBroadcast?(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
        createSession?(sessionId: string, data: unknown): boolean;
        resumeSession?(sessionId: string): unknown | null;
        publishEvent?(type: string, payload: unknown): Promise<boolean>;
        sendEvent?(type: string, payload: unknown, metadata?: ProtocolMetadata): boolean;
        sendProtocolError?(code: string, message: string, metadata?: ProtocolMetadata): boolean;
        sendBinaryEvent?(value: unknown): Promise<boolean>;
    };

    export interface RedWebConnectionContext extends RequestContext {
        readonly connectionId: string;
        readonly principal: unknown;
        session: unknown | null;
        metadata: Record<string, unknown>;
        readonly protocol?: Readonly<{ version: string }>;
    }

    export interface AdmissionContext {
        signal: AbortSignal;
        networkIdentity: string;
        route: SocketRoute;
    }

    export interface AdmissionOptions {
        authenticate?: (
            request: import('http').IncomingMessage,
            context: AdmissionContext
        ) => unknown | false | Promise<unknown | false>;
        origins?: string[] | ((
            origin: string | undefined,
            request: import('http').IncomingMessage
        ) => boolean | Promise<boolean>);
        place?: (
            principal: unknown,
            request: import('http').IncomingMessage,
            context: AdmissionContext
        ) => string | false | null | undefined | Promise<string | false | null | undefined>;
        allowedPlacementOrigins?: string[];
        allowInsecurePlacement?: boolean;
        timeoutMs?: number;
    }

    export interface MessageRateLimit {
        capacity: number;
        refillPerSecond: number;
        action?: 'drop' | 'disconnect';
    }

    export interface TransportLimits {
        maxConnections?: number;
        maxBufferedBytes?: number;
        maxPendingMessages?: number;
        messageRate?: MessageRateLimit;
        slowConsumerAction?: 'drop' | 'disconnect';
    }

    export interface HeartbeatOptions {
        intervalMs: number;
        timeoutMs: number;
    }

    export type RoomOptions = {
        maxRooms?: number;
        maxMembersPerRoom?: number;
        maxRoomsPerConnection?: number;
        maxRoomIdLength?: number;
    } & ({ authorize?: undefined; authorizationTimeoutMs?: never; maxPendingAuthorizations?: never; maxPendingPerConnection?: never } | {
        /** Grants subscription until explicit leave/disconnect; not per-message receive authorization. */
        authorize: (context: Readonly<RedWebConnectionContext>, roomId: string) => boolean | Promise<boolean>;
        authorizationTimeoutMs?: number;
        maxPendingAuthorizations?: number;
        maxPendingPerConnection?: number;
    });

    export interface SessionOptions {
        ttlMs?: number;
        maxSessions?: number;
        maxSessionIdLength?: number;
        sweepIntervalMs?: number;
    }

    export interface MetricsSink {
        increment?(name: string, value: number, attributes: Readonly<{ route: string }>): void | Promise<void>;
        gauge?(name: string, value: number, attributes: Readonly<{ route: string }>): void | Promise<void>;
        observe?(name: string, value: number, attributes: Readonly<{ route: string }>): void | Promise<void>;
    }

    export interface DistributionEvent<T = unknown> {
        id: string;
        source: string;
        type: string;
        payload: T;
    }

    export interface DistributionAdapter {
        start?(signal?: AbortSignal): void | Promise<void>;
        publish(channel: string, serializedEvent: string, signal?: AbortSignal): void | Promise<void>;
        subscribe(
            channel: string,
            onEvent: (serializedEvent: string | DistributionEvent) => void,
            signal?: AbortSignal
        ): void | (() => void | Promise<void>) | Promise<void | (() => void | Promise<void>)>;
        unsubscribe?(channel: string, signal?: AbortSignal): void | Promise<void>;
        close?(signal?: AbortSignal): void | Promise<void>;
    }

    export interface DistributionOptions {
        adapter: DistributionAdapter;
        channel: string;
        nodeId?: string;
        maxEventBytes?: number;
        maxSeenEvents?: number;
        seenTtlMs?: number;
        lifecycleTimeoutMs?: number;
        publishTimeoutMs?: number;
        maxConcurrentPublishes?: number;
        maxConcurrentEvents?: number;
        required?: boolean;
        onEvent(event: DistributionEvent, route: SocketRoute): void | Promise<void>;
    }

    export interface ProtocolMetadata {
        requestId?: string;
        sequence?: number;
    }

    export interface ProtocolBinaryCodec {
        maxBytes?: number;
        encode(value: unknown, context: RedWebConnectionContext): Buffer | Uint8Array | ArrayBuffer | Promise<Buffer | Uint8Array | ArrayBuffer>;
        decode(buffer: Buffer, context: RedWebConnectionContext): unknown | Promise<unknown>;
    }

    export interface ProtocolOptions {
        versions: readonly string[];
        required?: boolean;
        queryParameter?: string;
        header?: string;
        binary?: false | ProtocolBinaryCodec;
    }

    /** ─────────────────── SOCKET SERVER ─────────────────── */

    export interface DevelopmentOptions {
        /** Explicit local-only inspection; rejected when NODE_ENV is production. */
        inspect?: boolean;
    }

    export interface LiveDevelopmentOptions extends DevelopmentOptions {
        /** Loopback-only browser refresh. False overrides REDWEB_DEV_REFRESH=1. */
        refresh?: boolean;
    }

    export interface InspectionList<T> {
        readonly items: readonly T[];
        readonly total: number;
        readonly truncated: boolean;
    }
    export type InspectionSection<T> = { readonly available: false } | ({ readonly available: true } & T);
    export interface InspectionMembers {
        readonly className: string;
        readonly actions: InspectionList<string>;
        readonly states: InspectionList<string>;
    }
    export interface InspectionPage extends InspectionMembers {
        readonly path: string;
        readonly live: boolean;
        readonly shared: boolean;
        readonly instanceMetadata: 'observed' | 'unobserved';
        readonly instances: InspectionList<{
            readonly id: number;
            readonly disposed: boolean;
            readonly components: InspectionList<InspectionMembers & { readonly id: string }>;
        }>;
    }
    export interface InspectionSession {
        /** Inspector-local IDs; never page tokens, credentials or socket IDs. */
        readonly render: number;
        readonly instance: number;
        readonly route: string;
        readonly status: 'connected' | 'detaching' | 'pending' | 'retained';
        readonly reactive: boolean;
    }
    export interface InspectionEvent {
        readonly sequence: number;
        readonly render: number;
        readonly route: string;
        readonly kind: 'state-invalidated' | 'flush-started' | 'flush-completed' | 'flush-superseded' | 'flush-failed';
        readonly state?: string;
        readonly component?: string;
        readonly affectedOwners?: InspectionList<string>;
        readonly snapshot?: boolean;
        readonly dirtyOwners?: InspectionList<string>;
        readonly durationMs?: number;
    }
    export interface DevelopmentSnapshot {
        readonly schemaVersion: 1;
        readonly mode: 'development';
        readonly pages: InspectionSection<{
            readonly registrations: InspectionList<InspectionPage>;
            readonly sessions: InspectionList<InspectionSession>;
            readonly closing?: boolean;
            readonly rendering?: number;
            readonly connections?: Readonly<Record<InspectionSession['status'], number>>;
        }>;
        readonly sockets: InspectionSection<{
            readonly routes: InspectionList<{
                readonly path: string;
                readonly handlers: InspectionList<string>;
                readonly registeredConnections: number;
                readonly draining: boolean;
                readonly rooms: number;
                readonly sessions: number;
            }>;
            readonly pendingUpgrades: number;
            readonly draining: boolean;
        }>;
        /** Flush completion is not a network delivery guarantee. */
        readonly history: InspectionList<InspectionEvent> & { readonly limit: number };
    }

    export interface SocketServerOptions {
        development?: DevelopmentOptions;
        server?: NodeHttpServer;
        port?: number;
        bind?: string;
        listen?: boolean;
        routes?: Array<new () => SocketRoute>;
        ssl?: { key: string; cert: string };
        fallbackToRoot?: boolean;
        closeServerOnShutdown?: boolean;
        listenCallback?: () => void;
        logger?: RedWebLogger | null;
    }

    export interface RedWebLogger {
        log?(message?: any, ...optionalParams: any[]): void;
        warn?(message?: any, ...optionalParams: any[]): void;
        error?(message?: any, ...optionalParams: any[]): void;
    }

    /** ─────────────────── ROUTES & HANDLERS ─────────────────── */

    export interface SocketRouteConfig {
        path: string;
        handlers: Array<new () => BaseHandler>;
        services?: Array<new () => SocketService>;
        allowDuplicateConnections?: boolean;
        websocketOptions?: Omit<ServerOptions, 'noServer' | 'path' | 'server' | 'port'>;
        trustProxy?: boolean;
        getClientKey?: (request: import('http').IncomingMessage) => string;
        exposeErrors?: boolean;
        logger?: RedWebLogger | null;
        shutdownTimeoutMs?: number;
        admission?: AdmissionOptions | AdmissionOptions['authenticate'];
        limits?: TransportLimits;
        orderedMessages?: boolean;
        heartbeat?: HeartbeatOptions;
        rooms?: boolean | RoomOptions;
        sessions?: boolean | SessionOptions;
        metrics?: MetricsSink;
        distribution?: false | DistributionOptions;
        drainHandlers?: boolean;
        protocol?: false | ProtocolOptions;
        maxPendingUpgrades?: number;
    }

    /** Socket‑side autonomous service (game loops, timers, etc.) */
    export abstract class SocketService {
        name: string;
        tickRateMs: number | null;
        route: SocketRoute;
        protected _tickHandle: NodeJS.Timeout | null;

        constructor(name: string, tickRateMs?: number);

        /** Called once when the route is initialised */
        onInit(route: SocketRoute): void;

        /** Optional recurring tick (respecting tickRateMs) */
        onTick?(...args: any[]): unknown;

        /** Called on process shutdown / route removal */
        onShutdown(): void;
    }

    export abstract class FixedStepService extends SocketService {
        maxCatchUpTicks: number;
        maxRetainedLagMs: number;
        tick: number;
        accumulatorMs: number;

        constructor(name: string, tickRateMs: number, maxCatchUpTicks?: number, maxRetainedLagMs?: number);
        onTick?(stepMs: number, tick: number): void | Promise<void>;
        onLagDropped?(droppedLagMs: number): void;
        pulse(): Promise<void>;
        onShutdown(): Promise<void>;
    }

    /** Message handler, triggered by client messages */
    export class BaseHandler {
        name: string;
        constructor(name: string);

        handleMessage(
            socket: RedWebSocket,
            message: any
        ): Promise<unknown>;

        validateMessage(message: any, socket: RedWebSocket): boolean | Promise<boolean>;
        onMessage(socket: RedWebSocket, message: any): unknown;
        acceptsBinary?(socket: RedWebSocket, buffer: Buffer): boolean;
        handleBinaryMessage(socket: RedWebSocket, buffer: Buffer): Promise<unknown>;
        onBinaryMessage(socket: RedWebSocket, buffer: Buffer): unknown;
        onInitialContact(socket: RedWebSocket, request?: import('http').IncomingMessage): unknown;
    }

    export class SocketRoute {
        path: string;
        handlers: BaseHandler[];
        services: SocketService[];
        clients: Map<string, RedWebSocket>;
        rooms: RoomRegistry | null;
        sessions: SessionRegistry | null;
        distribution: unknown | null;
        protocolPolicy: unknown | null;
        draining: boolean;
        allowDuplicateConnections?: boolean;
        websocketOptions?: SocketRouteConfig['websocketOptions'];

        constructor(config: SocketRouteConfig);

        addHandler(handler: new () => BaseHandler): boolean;
        resolveRemoteAddress(request: import('http').IncomingMessage): string;
        connectionOpenCallback(socket: RedWebSocket, request?: import('http').IncomingMessage): unknown;
        connectionCloseCallback?(socket: RedWebSocket): unknown;
        handleMessage(sock: RedWebSocket, data: any): Promise<boolean>;
        handleBinaryMessage(socket: RedWebSocket, buffer: Buffer): Promise<boolean>;
        beginDrain(): boolean;
        isReady(): boolean;
        publish(type: string, payload: unknown): Promise<boolean>;
        shutdown(): Promise<void>;
    }

    /** ─────────────────── SERVER BASE ─────────────────── */

    export class BaseSocketServer {
        server: NodeHttpServer;
        routes: SocketRoute[];
        ownsServer: boolean;

        constructor(server: NodeHttpServer, options?: SocketServerOptions);

        addRoute(route: new () => SocketRoute): SocketRoute;
        isReady(): boolean;
        inspect(): DevelopmentSnapshot | null;
        beginDrain(): boolean;
        shutdown(): Promise<void>;
    }

    /** ─────────────────── REGISTRY & UTIL TYPES ─────────────────── */

    export function sendJson(socket: WebSocket, data: unknown): boolean;

    export class RoomRegistry {
        constructor(options?: RoomOptions);
        join(roomId: string, socket: RedWebSocket): boolean;
        enter(roomId: string, socket: RedWebSocket): Promise<boolean>;
        leave(roomId: string, socket: RedWebSocket): boolean;
        leaveAll(socket: RedWebSocket): number;
        members(roomId: string): RedWebSocket[];
        has(roomId: string, socket: RedWebSocket): boolean;
        broadcast(roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
        broadcastFrom(socket: RedWebSocket, roomId: string, data: unknown, options?: { except?: RedWebSocket }): number;
        clear(): void;
        close(): boolean;
        readonly size: number;
    }

    export class SessionRegistry<T = unknown> {
        constructor(options?: SessionOptions, logger?: RedWebLogger | null);
        create(sessionId: string, data: T, socket?: RedWebSocket): boolean;
        resume(sessionId: string, socket: RedWebSocket): T | null;
        release(socket: RedWebSocket): boolean;
        remove(sessionId: string): boolean;
        get(sessionId: string): T | undefined;
        sweep(): void;
        stop(): void;
        readonly size: number;
    }

    export interface SocketWrapper {
        socket: RedWebSocket;
        id: string;
        send: (type: string, payload: Record<string, any>) => void;
        getSanitized?(): Record<string, any>;
    }

    export type SocketMessage = {
        type: string;
        [key: string]: any;
    };

    /** Generic event‑driven registry for socket objects */
    /** Generic event-driven registry for socket objects */
    export class SocketRegistry<T extends SocketWrapper = SocketWrapper> extends EventEmitter {
        protected items: T[];

        constructor();

        /** Adds a socket-bound object to the registry */
        add(item: T): void;

        /**
         * Removes a socket-bound object by reference or id (default key: 'id')
         * @param itemOrId Object or ID string
         * @param by Key name to match against (default is 'id')
         */
        remove(itemOrId: T | string, by?: keyof T): boolean;

        /** Returns a shallow copy of all registered items */
        all(): T[];

        /** Returns the number of registered items */
        count(): number;
    }

    /** ─────────────────── CONCRETE SERVERS ─────────────────── */

    export class SocketServer extends BaseSocketServer {
        constructor(options?: SocketServerOptions);
    }

    export class SecureSocketServer extends BaseSocketServer {
        constructor(options?: SocketServerOptions);
    }

    export class BaseHttpServer {
        app: Application;
        server?: NodeHttpServer;
        constructor(options?: RedWebOptions);
        shutdown?(): Promise<void>;
    }

    export class HttpServer extends BaseHttpServer {
        constructor(options?: RedWebOptions);
        shutdown(): Promise<void>;
    }

    export class HttpsServer extends BaseHttpServer {
        constructor(options?: RedWebOptions);
        shutdown(): Promise<void>;
    }

    /** ─────────────────── LIVE HTML ─────────────────── */

    const htmlFragmentBrand: unique symbol;
    const htmlAttributeBrand: unique symbol;
    const htmlUrlBrand: unique symbol;

    export interface HtmlFragment {
        readonly [htmlFragmentBrand]: true;
        toString(): string;
    }

    export interface HtmlAttribute {
        readonly [htmlAttributeBrand]: true;
    }

    export interface HtmlUrl {
        readonly [htmlUrlBrand]: true;
    }

    export interface RedWebRequest {
        readonly path: string;
        readonly url: string;
        readonly method: string;
        readonly headers: Readonly<Record<string, string | readonly string[] | undefined>>;
        readonly params: Readonly<Record<string, string>>;
        readonly query: Readonly<Record<string, unknown>>;
        readonly body: unknown;
        get(name: string): string | undefined;
    }

    export interface LivePageRequest extends RedWebRequest {}

    export interface RequestContext<Principal = unknown> {
        readonly request: RedWebRequest;
        readonly params: Readonly<Record<string, string>>;
        readonly query: Readonly<Record<string, unknown>>;
        readonly body: unknown;
        readonly principal?: Principal;
        readonly signal: AbortSignal;
    }

    export interface LivePageRequestContext extends RequestContext<string | number | bigint | boolean> {}

    /** The original page request/identity is retained across normal reconnects. */
    export interface LivePageConnectionContext extends LivePageRequestContext {
        socket: RedWebSocket;
    }

    export abstract class LivePage {
        protected readonly _connections: Set<RedWebSocket>;
        loading?(context: LivePageRequestContext): void | Promise<void>;
        render?(context: LivePageRequestContext): string | HtmlFragment | Promise<string | HtmlFragment>;
        connected?(context: LivePageConnectionContext): void | Promise<void>;
        disconnected?(context: LivePageConnectionContext): void | Promise<void>;
        disposed?(): void | Promise<void>;
        dispose(): Promise<boolean>;
    }

    export type PageOptions = {
        template?: string;
        css?: string | readonly string[];
        live?: boolean;
        head?: PageHead;
        cache?: PageCache;
        layout?: PageLayout;
    } & ({
        scope?: 'connection' | 'shared'; shared?: boolean;
        authorize?: undefined; authorizationTimeoutMs?: never;
    } | {
        scope?: 'connection'; shared?: false;
        /** Checked before construction/loading, on connection, and before actions/state writes. */
        authorize: (context: LivePageRequestContext) => boolean | Promise<boolean>;
        authorizationTimeoutMs?: number;
    });

    export type PageLayout = (content: HtmlFragment, context: LivePageRequestContext) => HtmlFragment;

    export interface PageHead {
        title?: string;
        description?: string;
        canonical?: string;
        image?: string;
        robots?: string;
    }

    export interface PageCache {
        maxAge?: number;
        staleWhileRevalidate?: number;
        immutable?: boolean;
    }

    export interface StateOptions {
        writable?: boolean;
    }

    export interface LiveStateDecorator {
        (target: object, propertyKey: string): void;
        <This, Value>(value: undefined, context: ClassFieldDecoratorContext<This, Value>):
            (this: This, initialValue: Value) => Value;
    }

    export interface LiveActionDecorator {
        (target: object, propertyKey: string, descriptor: PropertyDescriptor): void | PropertyDescriptor;
        <This, Value extends (this: This, ...args: any[]) => any>(
            value: Value,
            context: ClassMethodDecoratorContext<This, Value>
        ): Value;
    }

    /** The validated (possibly transformed) value passed as an action's first argument. */
    export type ActionInput<Schema extends import('redweb/contract').SocketSchema> = import('redweb/contract').ContractOutput<Schema>;

    export interface ValidatedActionDecorator<Input> {
        <Value extends (input: Input, context: LivePageConnectionContext) => any>(target: object, propertyKey: string,
            descriptor: TypedPropertyDescriptor<Value>): void;
        <This, Value extends (this: This, input: Input, context: LivePageConnectionContext) => any>(
            value: Value, context: ClassMethodDecoratorContext<This, Value>
        ): Value;
    }

    export interface LiveViewDecorator {
        (target: object, propertyKey: string, descriptor: PropertyDescriptor): void | PropertyDescriptor;
        <This, Value extends (this: This, item: any, index: number) => HtmlFragment>(
            value: Value,
            context: ClassMethodDecoratorContext<This, Value>
        ): Value;
    }

    export function page(path: string, options?: PageOptions): ClassDecorator;
    export function component(): ClassDecorator;
    export function component<Props = void>(render: (properties: Props) => HtmlFragment):
        (properties: Props) => HtmlFragment;
    export function state(options?: StateOptions): LiveStateDecorator;
    export function action(): LiveActionDecorator;
    export interface ActionAuthorization<Input> {
        authorize: (context: LivePageConnectionContext, input: Input) => boolean | Promise<boolean>;
        /** Bounds permission checks, not application execution; defaults to 5000ms. */
        authorizationTimeoutMs?: number;
    }
    export function action<Schema extends import('redweb/contract').SocketSchema>(options: {
        input: Schema;
        /** Bounds input validation, not application execution; defaults to 5000ms. */
        validationTimeoutMs?: number;
    } & (ActionAuthorization<ActionInput<Schema>> | { authorize?: undefined; authorizationTimeoutMs?: never })): ValidatedActionDecorator<ActionInput<Schema>>;
    /** Authorized actions use a fixed (input, context) shape, including buttons without a payload. */
    export function action(options: ActionAuthorization<unknown>): ValidatedActionDecorator<unknown>;
    export function view(stateName: string): LiveViewDecorator;
    export function html(strings: TemplateStringsArray, ...values: unknown[]): HtmlFragment;
    export function attribute(value: string | number | bigint | boolean): HtmlAttribute;
    export function url(value: string): HtmlUrl;
    export function each<Item>(items: readonly Item[], render: (item: Item, index: number) => HtmlFragment): HtmlFragment;
    export function codeBlock(code: unknown, options?: {
        language?: string;
        label?: string;
        highlight?: (source: string, language: string) => HtmlFragment;
    }): HtmlFragment;

    export type LivePageClass = new () => object;

    export interface LiveHtmlServerBaseOptions extends Omit<RedWebOptions, 'enableHtmxRendering'> {
        development?: LiveDevelopmentOptions;
        pages: readonly LivePageClass[];
        templateRoot?: string;
        livePaths?: {
            socket?: string;
            client?: string;
            runtime?: string;
            css?: string;
        };
        sessionTtlMs?: number;
        maxSessions?: number;
        maxConcurrentRenders?: number;
        /** Phase-local render/route and final owned-HTTP cleanup bound; defaults to 1000ms, not a total application deadline. */
        shutdownTimeoutMs?: number;
        heartbeat?: HeartbeatOptions;
        origins?: string[] | ((origin: string | undefined, request: import('http').IncomingMessage) => boolean | Promise<boolean>);
    }

    export type LiveHtmlAuthentication = {
        authenticate(request: import('http').IncomingMessage | import('express').Request):
            string | number | bigint | boolean | null | undefined |
            Promise<string | number | bigint | boolean | null | undefined>;
        /** Bounds identity lookup, not external application work; defaults to 5000ms. */
        authenticationTimeoutMs?: number;
    } | { authenticate?: undefined; authenticationTimeoutMs?: never };
    export type LiveHtmlServerOptions = LiveHtmlServerBaseOptions & LiveHtmlAuthentication;
    export type LiveHtmlStartOptions = Omit<LiveHtmlServerBaseOptions, 'pages'> & LiveHtmlAuthentication;

    export class LiveHtmlServer {
        app: Application;
        server: NodeHttpServer | NodeHttpsServer;
        http: HttpServer | HttpsServer;
        sockets: SocketServer | null;
        constructor(options: LiveHtmlServerOptions);
        /** Revoke matching in-process sessions/renders; credential invalidation remains application-owned. */
        revoke(principal: string | number | bigint | true): Promise<number>;
        /** Local metadata only; null unless development inspection was explicitly enabled. */
        inspect(): DevelopmentSnapshot | null;
        shutdown(): Promise<void>;
    }

    export function start(
        pageOrPages: LivePageClass | readonly LivePageClass[],
        options?: LiveHtmlStartOptions
    ): LiveHtmlServer;

    export interface StaticExportOptions {
        outDir: string;
        templateRoot?: string;
        logger?: RedWebLogger | null;
    }

    export interface StaticExportResult {
        readonly pages: readonly string[];
        readonly assets: readonly string[];
    }

    export function exportStatic(
        pageOrPages: LivePageClass | readonly LivePageClass[],
        options: StaticExportOptions
    ): Promise<StaticExportResult>;

    export interface SiteOptions {
        origin?: string;
        css?: string | readonly string[];
        head?: PageHead;
        cache?: PageCache;
        layout?: PageLayout;
    }

    export type SitePageOptions = PageOptions & { live?: false };

    export interface SiteExportOptions extends StaticExportOptions {
        publicDir?: string;
    }

    export interface StaticSite {
        page(path: string, options?: SitePageOptions): ClassDecorator;
        export(
            pageOrPages: LivePageClass | readonly LivePageClass[],
            options: SiteExportOptions
        ): Promise<StaticExportResult>;
    }

    export function defineSite(options?: SiteOptions): StaticSite;

    export class HtmlRenderer {
        static template(filePath: string, rootDir: string): string;
        static stylesheet(filePath: string, rootDir: string): string;
        static render(source: string, page: object, options?: { live?: boolean }): string;
        static collection(page: object, name: string, value: unknown): string;
        static statePayload(name: string, value: unknown, page?: object): { name: string; value: string; html: boolean };
        static head(metadata?: PageHead): string;
        static document(
            markup: string,
            config?: (Record<string, unknown> & { runtimePath: string }) | null,
            stylesheets?: string[],
            metadata?: PageHead
        ): string;
    }

    /** ─────────────────── CONSTANTS ─────────────────── */

    export const METHODS: {
        GET: 'get';
        POST: 'post';
        PUT: 'put';
        PATCH: 'patch';
        DELETE: 'delete';
        OPTIONS: 'options';
        HEAD: 'head';
        ALL: 'all';
    };

    export const ENCODINGS: {
        json: 'json';
        urlencoded: 'urlencoded';
    };

    export const HTTP_OPTIONS: RedWebOptions;
    export const SOCKET_OPTIONS: SocketServerOptions;

    export const ERROR_CODES: typeof import('redweb/client').ERROR_CODES;
}

client.d.ts

ts
// Generated from src/ws/protocol-schema.json by scripts/generate-protocol-types.js.
export type RedWebProtocolErrorCode =
    | 'INVALID_MESSAGE'
    | 'INVALID_PAYLOAD'
    | 'UNKNOWN_HANDLER'
    | 'HANDLER_FAILED'
    | 'BINARY_UNSUPPORTED'
    | 'RATE_LIMITED'
    | 'QUEUE_FULL'
    | 'CAPACITY_REACHED'
    | 'INITIALIZATION_FAILED'
    | 'ACCESS_DENIED'
    | 'ACCESS_TIMEOUT'
    | 'ACCESS_CANCELLED'
    | 'ACCESS_CAPACITY';

export interface ProtocolMetadata {
    requestId?: string;
    sequence?: number;
}

export interface ProtocolEnvelope<T = unknown> extends ProtocolMetadata {
    v: string;
    type: string;
    payload: T;
}

export interface ProtocolErrorEnvelope extends ProtocolMetadata {
    v: string;
    type: 'error';
    error: { code: RedWebProtocolErrorCode | string; message: string };
}

export interface SendableSocket {
    send(data: string): unknown;
}

export class ProtocolClient {
    constructor(socket: SendableSocket, version: string);
    readonly socket: SendableSocket;
    readonly version: string;
    envelope<T>(type: string, payload: T, metadata?: ProtocolMetadata): ProtocolEnvelope<T>;
    send<T>(type: string, payload: T, metadata?: ProtocolMetadata): void;
    parse<T = unknown>(input: string | Uint8Array | ArrayBuffer | { data: string | Uint8Array | ArrayBuffer }): ProtocolEnvelope<T> | ProtocolErrorEnvelope;
}

export const ERROR_CODES: { readonly [Code in RedWebProtocolErrorCode]: Code };

contract.d.ts

ts
import type { BaseHandler, ProtocolMetadata, RedWebSocket } from 'redweb';
import type { ProtocolEnvelope, ProtocolErrorEnvelope, SendableSocket } from './client';

/** Structural Standard Schema v1 support; use an existing compatible validator library. */
export interface SocketSchema<Input = unknown, Output = Input> {
    readonly '~standard': {
        readonly version: 1;
        readonly validate: (input: unknown) =>
            { readonly value: Output; readonly issues?: undefined } | { readonly issues: readonly unknown[] } |
            Promise<{ readonly value: Output; readonly issues?: undefined } | { readonly issues: readonly unknown[] }>;
        readonly types?: { readonly input: Input; readonly output: Output };
    };
}

export type SocketSchemas = Readonly<Record<string, SocketSchema>>;
export type ContractInput<Schema extends SocketSchema> = NonNullable<Schema['~standard']['types']>['input'];
export type ContractOutput<Schema extends SocketSchema> = Awaited<NonNullable<Schema['~standard']['types']>['output']>;
export type ContractMessage<Schemas extends SocketSchemas> = {
    [Type in keyof Schemas & string]: ProtocolEnvelope<ContractOutput<Schemas[Type]>> & { type: Type };
}[keyof Schemas & string];

export interface ContractClient<Schemas extends SocketSchemas> {
    envelope<Type extends keyof Schemas & string>(type: Type, payload: ContractInput<Schemas[Type]>, metadata?: ProtocolMetadata):
        Promise<ProtocolEnvelope<ContractInput<Schemas[Type]>> & { type: Type }>;
    send<Type extends keyof Schemas & string>(type: Type, payload: ContractInput<Schemas[Type]>, metadata?: ProtocolMetadata): Promise<void>;
    parse(input: string | Uint8Array | ArrayBuffer | { data: string | Uint8Array | ArrayBuffer }):
        Promise<ContractMessage<Schemas> | ProtocolErrorEnvelope>;
}

export interface SocketContract<Schemas extends SocketSchemas> {
    readonly version: string;
    readonly types: readonly (keyof Schemas & string)[];
    readonly validationTimeoutMs: number;
    readonly protocol: { readonly versions: readonly string[] };
    parse<Type extends keyof Schemas & string>(type: Type, payload: unknown): Promise<ContractOutput<Schemas[Type]>>;
    handler<Type extends keyof Schemas & string>(type: Type, callback: (
        socket: RedWebSocket, payload: ContractOutput<Schemas[Type]>,
        message: ProtocolEnvelope<ContractOutput<Schemas[Type]>> & { type: Type },
    ) => unknown): new () => BaseHandler;
    client(socket: SendableSocket): ContractClient<Schemas>;
    send<Type extends keyof Schemas & string>(socket: RedWebSocket, type: Type, payload: ContractInput<Schemas[Type]>, metadata?: ProtocolMetadata): Promise<boolean>;
}

export function defineSocketContract<const Schemas extends SocketSchemas>(version: string, schemas: Schemas,
    options?: { validationTimeoutMs?: number }): SocketContract<Schemas>;

jsx-runtime.d.ts

ts
import type { HtmlFragment, LivePageRequestContext } from 'redweb';

/** A synchronous @component() instance owned by a page/component field. Ownership is checked at runtime. */
export interface ServerComponent {
    render(context: LivePageRequestContext): string | HtmlFragment | readonly HtmlFragment[];
}

export type Child = HtmlFragment | ServerComponent | string | number | bigint | boolean | null | undefined | readonly Child[];

export interface IntrinsicAttributes {
    key?: string | number;
}

export interface IntrinsicProperties extends IntrinsicAttributes {
    children?: Child;
    class?: string;
    className?: string;
    id?: string;
    htmlFor?: string;
    [name: string]: unknown;
}

export namespace JSX {
    type Element = HtmlFragment;
    interface ElementChildrenAttribute { children: {}; }
    interface IntrinsicAttributes { key?: string | number; }
    interface IntrinsicElements { [name: string]: IntrinsicProperties; }
}

export const Fragment: unique symbol;
export type ElementType = string | typeof Fragment | ((properties: any) => HtmlFragment | readonly HtmlFragment[]);
export function jsx(type: ElementType, properties: IntrinsicProperties | null, key?: string | number): HtmlFragment;
export const jsxs: typeof jsx;

jsx-dev-runtime.d.ts

ts
export { Fragment } from './jsx-runtime';
export type { Child, ElementType, IntrinsicAttributes, IntrinsicProperties, JSX } from './jsx-runtime';
import type { HtmlFragment } from 'redweb';
import type { ElementType, IntrinsicProperties } from './jsx-runtime';

export function jsxDEV(
    type: ElementType,
    properties: IntrinsicProperties | null,
    key?: string | number,
    isStaticChildren?: boolean,
    source?: unknown,
    self?: unknown,
): HtmlFragment;