This commit is contained in:
elpresidank 2026-04-05 21:09:33 -05:00
parent 9e9307a2aa
commit e26caa0b12
123 changed files with 3478 additions and 10078 deletions

View file

@ -0,0 +1,110 @@
/**
* Dispatcher manager routes requests to backend services via pub/sub.
*
* Python reference: trustgraph-flow/trustgraph/gateway/dispatch/manager.py
*/
import { NatsBackend, RequestResponse, type PubSubBackend } from "@trustgraph/base";
import type { GatewayConfig } from "../server.js";
export type Responder = (response: unknown, complete: boolean) => Promise<void>;
export class DispatcherManager {
private pubsub: PubSubBackend;
private requestors = new Map<string, RequestResponse<unknown, unknown>>();
constructor(private readonly config: GatewayConfig) {
this.pubsub = new NatsBackend(config.natsUrl ?? "nats://localhost:4222");
}
async start(): Promise<void> {
// Pre-create requestors for known global services
// Flow-specific requestors are created on demand
}
async stop(): Promise<void> {
for (const rr of this.requestors.values()) {
await rr.stop();
}
await this.pubsub.close();
}
private async getRequestor(
requestTopic: string,
responseTopic: string,
key: string,
): Promise<RequestResponse<unknown, unknown>> {
let rr = this.requestors.get(key);
if (!rr) {
rr = new RequestResponse({
pubsub: this.pubsub,
requestTopic,
responseTopic,
subscription: `gateway-${key}`,
});
await rr.start();
this.requestors.set(key, rr);
}
return rr;
}
async dispatchGlobalService(
kind: string,
request: Record<string, unknown>,
): Promise<unknown> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const rr = await this.getRequestor(requestTopic, responseTopic, `global:${kind}`);
return rr.request(request);
}
async dispatchFlowService(
flow: string,
kind: string,
request: Record<string, unknown>,
): Promise<unknown> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const rr = await this.getRequestor(requestTopic, responseTopic, `flow:${flow}:${kind}`);
return rr.request(request);
}
async dispatchGlobalServiceStreaming(
kind: string,
request: Record<string, unknown>,
responder: Responder,
): Promise<void> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const rr = await this.getRequestor(requestTopic, responseTopic, `global:${kind}`);
await rr.request(request, {
recipient: async (response) => {
const res = response as Record<string, unknown>;
const complete = !!res.complete || !!res.endOfStream || !!res.endOfSession;
await responder(res, complete);
return complete;
},
});
}
async dispatchFlowServiceStreaming(
flow: string,
kind: string,
request: Record<string, unknown>,
responder: Responder,
): Promise<void> {
const requestTopic = `tg.flow.${kind}-request`;
const responseTopic = `tg.flow.${kind}-response`;
const rr = await this.getRequestor(requestTopic, responseTopic, `flow:${flow}:${kind}`);
await rr.request(request, {
recipient: async (response) => {
const res = response as Record<string, unknown>;
const complete = !!res.complete || !!res.endOfStream || !!res.endOfSession;
await responder(res, complete);
return complete;
},
});
}
}

View file

@ -0,0 +1,86 @@
/**
* WebSocket multiplexer handles concurrent requests over a single connection.
*
* Python reference: trustgraph-flow/trustgraph/gateway/dispatch/mux.py
*/
import { AsyncQueue } from "@trustgraph/base";
const MAX_OUTSTANDING = 15;
const MAX_QUEUE_SIZE = 10;
export interface MuxRequest {
id: string;
service: string;
flow?: string;
request: Record<string, unknown>;
}
export type MuxHandler = (
request: MuxRequest,
respond: (response: unknown, complete: boolean) => Promise<void>,
) => Promise<void>;
export class Mux {
private queue = new AsyncQueue<MuxRequest>();
private outstanding = 0;
private running = true;
constructor(private readonly handler: MuxHandler) {}
receive(request: MuxRequest): void {
if (this.queue.length >= MAX_QUEUE_SIZE) {
console.warn("[Mux] Queue full, dropping request:", request.id);
return;
}
this.queue.push(request);
}
async run(send: (data: string) => void): Promise<void> {
while (this.running) {
if (this.outstanding >= MAX_OUTSTANDING) {
await sleep(50);
continue;
}
try {
const request = await this.queue.pop(1000);
this.outstanding++;
// Fire and forget — error handling inside
this.processRequest(request, send).finally(() => {
this.outstanding--;
});
} catch {
// Timeout on queue pop — just loop
}
}
}
stop(): void {
this.running = false;
}
private async processRequest(
request: MuxRequest,
send: (data: string) => void,
): Promise<void> {
try {
await this.handler(request, async (response, complete) => {
send(JSON.stringify({ id: request.id, response, complete }));
});
} catch (err) {
send(
JSON.stringify({
id: request.id,
error: { type: "internal", message: String(err) },
complete: true,
}),
);
}
}
}
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}