Rename src to source
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
/** A normal request/response result before transport encoding. */
|
||||
export type StreamResponse = {
|
||||
/** Optional correlation ID for multiplexed transports. */
|
||||
id?: string;
|
||||
|
||||
/** Discriminator marking this message as a route response. */
|
||||
type: 'response';
|
||||
|
||||
/** HTTP-equivalent status code for the response body. */
|
||||
statusCode: number;
|
||||
|
||||
/** Serialized response payload. */
|
||||
body: unknown;
|
||||
};
|
||||
|
||||
/** An application event before a transport applies its wire encoding. */
|
||||
export type StreamEvent = {
|
||||
/** Optional event or correlation ID. */
|
||||
id?: string;
|
||||
|
||||
/** Application-defined event type name. */
|
||||
type: string;
|
||||
|
||||
/** Serialized event payload. */
|
||||
data: unknown;
|
||||
};
|
||||
|
||||
/** Union of every message a connection stream can emit. */
|
||||
export type StreamMessage = StreamResponse | StreamEvent;
|
||||
|
||||
/**
|
||||
* Connection-level output channel shared by route requests and the broadcaster.
|
||||
*
|
||||
* A WebSocket connection can back many request-scoped RouteStream instances.
|
||||
* SSE and normal HTTP each create one connection stream per request.
|
||||
*/
|
||||
export abstract class BaseStream {
|
||||
/** Whether this connection can remain open for server events. */
|
||||
abstract readonly streaming: boolean;
|
||||
|
||||
/** Whether later requests can modify this connection's subscriptions. */
|
||||
abstract readonly bidirectional: boolean;
|
||||
|
||||
/**
|
||||
* Send one response or event using the transport's wire encoding.
|
||||
*
|
||||
* @param message - Neutral response or event envelope to encode.
|
||||
*/
|
||||
abstract send(message: StreamMessage): Promise<void>;
|
||||
|
||||
/** Close the underlying connection and notify lifecycle observers. */
|
||||
abstract close(): void;
|
||||
|
||||
/**
|
||||
* Observe closure whether it occurs locally or at the remote peer.
|
||||
*
|
||||
* @param callback - Invoked once when the connection closes.
|
||||
*/
|
||||
abstract onClose(callback: () => void): void;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import { toExtendedJson } from '@xo-cash/utils';
|
||||
import { BaseStream, type StreamMessage } from './base-stream.ts';
|
||||
import type { SSEStreamingApi } from 'hono/streaming';
|
||||
|
||||
/** Maps neutral stream messages onto Hono's Server-Sent Events API. */
|
||||
export class HonoSSEStream extends BaseStream {
|
||||
readonly streaming = true;
|
||||
readonly bidirectional = false;
|
||||
|
||||
/** Observers notified when the SSE connection closes. */
|
||||
private readonly closeCallbacks: Array<() => void> = [];
|
||||
|
||||
/** Guards against sends after local closure. */
|
||||
private closed = false;
|
||||
|
||||
/**
|
||||
* @param stream - Hono SSE writer bound to the active HTTP response.
|
||||
*/
|
||||
constructor(private readonly stream: SSEStreamingApi) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* Encode and write one SSE event frame.
|
||||
*
|
||||
* @param message - Neutral response or event envelope to send.
|
||||
*/
|
||||
async send(message: StreamMessage): Promise<void> {
|
||||
if (this.closed) {
|
||||
throw new Error('Cannot send to a closed SSE stream');
|
||||
}
|
||||
|
||||
// SSE carries type and ID as protocol fields; the payload remains Extended JSON.
|
||||
await this.stream.writeSSE({
|
||||
event: message.type,
|
||||
data: toExtendedJson('body' in message ? message.body : message.data),
|
||||
...(message.id === undefined ? {} : { id: message.id }),
|
||||
});
|
||||
}
|
||||
|
||||
/** Close the SSE response and notify lifecycle observers. */
|
||||
async close(): Promise<void> {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
try {
|
||||
await this.stream.close();
|
||||
} catch (error) {
|
||||
const errorInstance = error instanceof Error ? error : new Error(String(error));
|
||||
throw errorInstance;
|
||||
}
|
||||
|
||||
this.emitClose();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback invoked when the stream closes.
|
||||
*
|
||||
* @param callback - Called immediately if the stream is already closed.
|
||||
*/
|
||||
onClose(callback: () => void): void {
|
||||
if (this.closed) {
|
||||
callback();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeCallbacks.push(callback);
|
||||
}
|
||||
|
||||
/** Drain and invoke all registered close observers. */
|
||||
private emitClose(): void {
|
||||
const callbacks = this.closeCallbacks.splice(0);
|
||||
for (const callback of callbacks) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import { BaseStream, type StreamMessage, type StreamResponse } from './base-stream.ts';
|
||||
|
||||
/**
|
||||
* One-shot connection stream used to run normal HTTP through the route API.
|
||||
*
|
||||
* Sending stores a response until the handler returns; no bytes are committed
|
||||
* early, so route errors can still replace it with the shared public error.
|
||||
*/
|
||||
export class HttpRequestStream extends BaseStream {
|
||||
readonly streaming = false;
|
||||
readonly bidirectional = false;
|
||||
|
||||
/** Buffered route response, committed only after dispatch completes. */
|
||||
private response: StreamResponse | undefined;
|
||||
|
||||
/** Observers notified when the logical request ends. */
|
||||
private readonly closeCallbacks: Array<() => void> = [];
|
||||
|
||||
/** Guards against sends after the request lifecycle ends. */
|
||||
private closed = false;
|
||||
|
||||
/**
|
||||
* Buffer exactly one route response for later HTTP encoding.
|
||||
*
|
||||
* @param message - Must be a response envelope, not an event.
|
||||
*/
|
||||
async send(message: StreamMessage): Promise<void> {
|
||||
if (this.closed) {
|
||||
throw new Error('Cannot send to a closed HTTP request');
|
||||
}
|
||||
|
||||
if (!('body' in message)) {
|
||||
throw new Error('Normal HTTP requests can only send a response');
|
||||
}
|
||||
|
||||
if (this.response) {
|
||||
throw new Error('Normal HTTP requests can only send one response');
|
||||
}
|
||||
|
||||
this.response = message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the buffered response for the transport adapter to encode.
|
||||
*
|
||||
* @returns The stored response, or undefined if the handler sent nothing.
|
||||
*/
|
||||
getResponse(): StreamResponse | undefined {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
/** End the logical request and notify lifecycle observers. */
|
||||
close(): void {
|
||||
if (this.closed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.closed = true;
|
||||
const callbacks = this.closeCallbacks.splice(0);
|
||||
callbacks.forEach((callback) => callback());
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a callback invoked when the request closes.
|
||||
*
|
||||
* @param callback - Called immediately if the request is already closed.
|
||||
*/
|
||||
onClose(callback: () => void): void {
|
||||
if (this.closed) {
|
||||
callback();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.closeCallbacks.push(callback);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user