import { Core } from "./core.js";
import { ClientError, clientError } from "./error.js";
import type { InboundFrame, OutboundMessage } from "./message.js";
import { defaultLimits, validateIds, type ConnectOptions, type Established, type Protocol, type ReadyOf, type Request, type RequestOptions } from "./protocol.js";
import { RawStream, Stream } from "./stream.js";
import { Attempt, checkSignal, wait } from "./timing.js";
import type { ByteTransport, Connector } from "./transport.js";
import type { RawFrame } from "./wire.js";
/** Generic framed connection; native, encoded, raw and packet APIs share one router. */
export class Client
{
private readonly release: () => void;
private constructor(private readonly core: Core
) { this.release = core.retain(this); }
/** Establish a protocol on a caller-owned, authenticated transport. */
static async connectTransport
(transport: ByteTransport, protocol: P, options: ConnectOptions = {}): Promise> {
let handedOff = false;
try {
return await Client.connectConnector({ connect: async () => { handedOff = true; return transport; } }, protocol, options);
} catch (error) {
// Ownership transfers at invocation, including invalid/pre-aborted setup.
if (!handedOff) await Promise.resolve().then(() => transport.close()).catch(() => undefined);
throw error;
}
}
/** One total setup deadline covers dialing and protocol establishment. */
static async connectConnector(connector: Connector, protocol: P, options: ConnectOptions = {}): Promise> {
const limits = defaultLimits(options.limits);
const timeoutMs = options.setupTimeoutMs ?? 10_000;
const attempt = new Attempt(timeoutMs, options.signal);
const context = { deadlineMs: performance.now() + timeoutMs, signal: attempt.signal, limits };
let transport: ByteTransport | undefined;
try {
checkSignal(attempt.signal);
const dialing = connector.connect(context).then(value => {
// Close a transport that a connector resolves after cancellation.
if (attempt.signal.aborted) { void value.close().catch(() => undefined); checkSignal(attempt.signal); }
transport = value;
return value;
});
transport = await wait(dialing, attempt.signal);
const established = await wait(protocol.establish(transport, context), attempt.signal) as Established>;
checkSignal(attempt.signal);
return await Client.fromEstablished(protocol, established);
} catch (error) {
await transport?.close().catch(() => undefined);
throw clientError(error).withDelivery("not_sent");
} finally { attempt.close(); }
}
/** Start routing an externally established transport using public types. */
static async fromEstablished(protocol: P, established: Established>): Promise> {
try {
validateIds(established.ids);
established.limits = defaultLimits(established.limits);
} catch (error) {
await Promise.resolve().then(() => established.transport.close()).catch(() => undefined);
throw error;
}
const core = new Core(protocol, established);
const client = new Client(core);
core.start();
return client;
}
get ready(): ReadyOf { return this.core.established.ready; }
clone(): Client
{ return new Client(this.core); }
isClosed(): boolean { return this.core.failure !== undefined; }
/** Close every shared handle; no automatic reconnect follows. */
async close(): Promise { this.release(); await this.core.close(); }
async request(message: OutboundMessage, options?: RequestOptions): Promise {
const outbound = this.core.prepare(message);
const frame = await this.requestRaw(outbound.flags, outbound.body, options);
try { return this.core.established.codec.decode(frame); }
catch { throw new ClientError("invalid_data", "unknown"); }
}
async requestTyped(request: Request, options?: RequestOptions): Promise {
const frame = await this.request(request.message(), options);
if (!frame.isTerminal()) throw new ClientError("invalid_data", "unknown");
return request.decode(frame);
}
async openStream(message: OutboundMessage, options?: RequestOptions): Promise> {
const outbound = this.core.prepare(message);
return new Stream(await this.openStreamRaw(outbound.flags, outbound.body, options));
}
async requestRaw(flags: number, body: Uint8Array, options: RequestOptions = {}): Promise {
const attempt = new Attempt(options.requestTimeoutMs ?? this.core.established.limits.requestTimeoutMs, options.signal);
let stream: RawStream | undefined;
try {
checkSignal(attempt.signal);
const lease = this.core.reserve();
stream = new RawStream(this.core, lease);
try {
await this.core.writeFrame(lease, flags, body, attempt.signal);
const frame = await stream.next({ signal: attempt.signal });
if (!frame) throw new ClientError("peer_closed");
return frame;
} catch (error) { throw clientError(error).withDelivery(this.core.delivery(lease)); }
} finally { stream?.close(); attempt.close(); }
}
async openStreamRaw(flags: number, body: Uint8Array, options: RequestOptions = {}): Promise> {
const attempt = new Attempt(options.requestTimeoutMs ?? this.core.established.limits.requestTimeoutMs, options.signal);
let stream: RawStream | undefined;
try {
checkSignal(attempt.signal);
const lease = this.core.reserve();
stream = new RawStream(this.core, lease);
try { await this.core.writeFrame(lease, flags, body, attempt.signal); }
catch (error) { throw clientError(error).withDelivery(this.core.delivery(lease)); }
return stream;
} catch (error) { stream?.close(); throw error; }
finally { attempt.close(); }
}
async sendOnStream(id: number, message: OutboundMessage, options?: RequestOptions): Promise {
const outbound = this.core.prepare(message);
await this.sendRaw(id, outbound.flags, outbound.body, options);
}
async sendRaw(id: number, flags: number, body: Uint8Array, options: RequestOptions = {}): Promise {
const lease = this.core.owned(id);
const attempt = new Attempt(options.requestTimeoutMs, options.signal);
try { await this.core.writeFrame(lease, flags, body, attempt.signal); }
finally { attempt.close(); }
}
async writeUnchecked(packet: Uint8Array | { bytes: Uint8Array }, options: RequestOptions = {}): Promise {
const attempt = new Attempt(options.requestTimeoutMs, options.signal);
try { await this.core.writeExact(packet instanceof Uint8Array ? packet : packet.bytes, attempt.signal); }
finally { attempt.close(); }
}
/** Local disposal sends no signal, EOF, or application cancellation. */
closeStream(id: number): void { this.core.closeStream(id); }
}