trustgraph/ts/packages/base/src/messaging/request-response.ts

95 lines
2.7 KiB
TypeScript
Raw Normal View History

2026-04-05 21:09:33 -05:00
/**
* Request/response pattern over pub/sub.
*
* Sends a request with a unique ID, subscribes for matching responses.
* Supports streaming (multiple responses per request) via a recipient callback.
*
* Python reference: trustgraph-base/trustgraph/base/request_response_spec.py
*/
import { randomUUID } from "node:crypto";
2026-06-01 20:26:47 -05:00
import { makeProducer, type Producer } from "./producer.js";
import { makeSubscriber, type Subscriber } from "./subscriber.js";
2026-04-05 21:09:33 -05:00
import type { PubSubBackend } from "../backend/types.js";
export interface RequestResponseOptions {
pubsub: PubSubBackend;
requestTopic: string;
responseTopic: string;
subscription: string;
}
2026-06-01 20:26:47 -05:00
export interface RequestResponse<TReq, TRes> {
readonly start: () => Promise<void>;
readonly stop: () => Promise<void>;
readonly request: (
2026-04-05 21:09:33 -05:00
request: TReq,
options?: {
timeoutMs?: number;
recipient?: (response: TRes) => Promise<boolean>;
},
2026-06-01 20:26:47 -05:00
) => Promise<TRes>;
}
2026-04-05 21:09:33 -05:00
2026-06-01 20:26:47 -05:00
export function makeRequestResponse<TReq, TRes>(
options: RequestResponseOptions,
): RequestResponse<TReq, TRes> {
const producer: Producer<TReq> = makeProducer<TReq>(options.pubsub, options.requestTopic);
const subscriber: Subscriber<TRes> = makeSubscriber<TRes>(
options.pubsub,
options.responseTopic,
options.subscription,
);
2026-04-05 21:09:33 -05:00
2026-06-01 20:26:47 -05:00
return {
start: async () => {
await producer.start();
await subscriber.start();
},
stop: async () => {
await producer.stop();
await subscriber.stop();
},
/**
* Send a request and wait for responses.
*
* @param request - The request payload
* @param options.timeoutMs - Total timeout in milliseconds (default: 300s)
* @param options.recipient - Optional callback for streaming responses.
* Return `true` to indicate the final response has been received.
* If omitted, returns the first response.
*/
request: async (request, requestOptions) => {
const id = randomUUID();
const timeoutMs = requestOptions?.timeoutMs ?? 300_000;
const recipient = requestOptions?.recipient;
2026-04-05 21:09:33 -05:00
2026-06-01 20:26:47 -05:00
const queue = subscriber.subscribe(id);
2026-04-05 21:09:33 -05:00
2026-06-01 20:26:47 -05:00
try {
await producer.send(id, request);
const deadline = Date.now() + timeoutMs;
2026-04-05 21:09:33 -05:00
2026-06-01 20:26:47 -05:00
while (true) {
const remaining = deadline - Date.now();
if (remaining <= 0) {
throw new Error(`Request timed out after ${timeoutMs}ms`);
}
2026-04-05 21:09:33 -05:00
2026-06-01 20:26:47 -05:00
const response = await queue.pop(remaining);
if (recipient !== undefined) {
const isFinal = await recipient(response);
if (isFinal) return response;
} else {
return response;
}
2026-04-05 21:09:33 -05:00
}
2026-06-01 20:26:47 -05:00
} finally {
subscriber.unsubscribe(id);
2026-04-05 21:09:33 -05:00
}
2026-06-01 20:26:47 -05:00
},
};
2026-04-05 21:09:33 -05:00
}