mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 17:39:39 +02:00
Migrate request-response facade to Effect runtime
This commit is contained in:
parent
7f81c56c80
commit
a0d2575273
5 changed files with 306 additions and 47 deletions
|
|
@ -7,10 +7,12 @@
|
|||
* Python reference: trustgraph-base/trustgraph/base/request_response_spec.py
|
||||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { makeProducer, type Producer } from "./producer.js";
|
||||
import { makeSubscriber, type Subscriber } from "./subscriber.js";
|
||||
import { Effect, Exit, Scope } from "effect";
|
||||
import type { PubSubBackend } from "../backend/types.js";
|
||||
import { PubSub } from "../backend/pubsub.js";
|
||||
import { messagingDeliveryError, messagingLifecycleError } from "../errors.js";
|
||||
import { loadMessagingRuntimeConfig } from "../runtime/messaging-config.js";
|
||||
import { makeEffectRequestResponseFromPubSub, type EffectRequestResponse } from "./runtime.js";
|
||||
|
||||
export interface RequestResponseOptions {
|
||||
pubsub: PubSubBackend;
|
||||
|
|
@ -31,24 +33,48 @@ export interface RequestResponse<TReq, TRes> {
|
|||
) => Promise<TRes>;
|
||||
}
|
||||
|
||||
interface RequestResponseRuntime<TReq, TRes> {
|
||||
readonly scope: Scope.Closeable;
|
||||
readonly requestor: EffectRequestResponse<TReq, TRes>;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
let runtime: RequestResponseRuntime<TReq, TRes> | null = null;
|
||||
|
||||
return {
|
||||
start: async () => {
|
||||
await producer.start();
|
||||
await subscriber.start();
|
||||
if (runtime !== null) return;
|
||||
|
||||
const scope = await Effect.runPromise(Scope.make());
|
||||
|
||||
try {
|
||||
const config = await Effect.runPromise(loadMessagingRuntimeConfig());
|
||||
const requestor = await Effect.runPromise(
|
||||
makeEffectRequestResponseFromPubSub<TReq, TRes>(
|
||||
PubSub.fromBackend(options.pubsub),
|
||||
config,
|
||||
{
|
||||
requestTopic: options.requestTopic,
|
||||
responseTopic: options.responseTopic,
|
||||
subscription: options.subscription,
|
||||
},
|
||||
).pipe(Scope.provide(scope)),
|
||||
);
|
||||
|
||||
runtime = { scope, requestor };
|
||||
} catch (error) {
|
||||
await Effect.runPromise(Scope.close(scope, Exit.fail(error))).catch(() => undefined);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
stop: async () => {
|
||||
await producer.stop();
|
||||
await subscriber.stop();
|
||||
const current = runtime;
|
||||
runtime = null;
|
||||
if (current === null) return;
|
||||
|
||||
await Effect.runPromise(Scope.close(current.scope, Exit.void));
|
||||
},
|
||||
/**
|
||||
* Send a request and wait for responses.
|
||||
|
|
@ -60,35 +86,32 @@ export function makeRequestResponse<TReq, TRes>(
|
|||
* If omitted, returns the first response.
|
||||
*/
|
||||
request: async (request, requestOptions) => {
|
||||
const id = randomUUID();
|
||||
const current = runtime;
|
||||
if (current === null) {
|
||||
throw messagingLifecycleError(
|
||||
`${options.requestTopic}:${options.responseTopic}`,
|
||||
"request",
|
||||
"RequestResponse not started",
|
||||
);
|
||||
}
|
||||
|
||||
const timeoutMs = requestOptions?.timeoutMs ?? 300_000;
|
||||
const recipient = requestOptions?.recipient;
|
||||
|
||||
const queue = subscriber.subscribe(id);
|
||||
|
||||
try {
|
||||
await producer.send(id, request);
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
|
||||
while (true) {
|
||||
const remaining = deadline - Date.now();
|
||||
if (remaining <= 0) {
|
||||
throw new Error(`Request timed out after ${timeoutMs}ms`);
|
||||
}
|
||||
|
||||
const response = await queue.pop(remaining);
|
||||
|
||||
if (recipient !== undefined) {
|
||||
const isFinal = await recipient(response);
|
||||
if (isFinal) return response;
|
||||
} else {
|
||||
return response;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
subscriber.unsubscribe(id);
|
||||
}
|
||||
return await Effect.runPromise(
|
||||
current.requestor.request(request, {
|
||||
timeoutMs,
|
||||
...(recipient === undefined
|
||||
? {}
|
||||
: {
|
||||
recipient: (response) =>
|
||||
Effect.tryPromise({
|
||||
try: () => recipient(response),
|
||||
catch: (error) => messagingDeliveryError(options.responseTopic, "recipient", error),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -424,7 +424,11 @@ export const makeEffectRequestResponseFromPubSub = Effect.fn("makeEffectRequestR
|
|||
config: MessagingRuntimeConfig,
|
||||
options: EffectRequestResponseOptions,
|
||||
) {
|
||||
const producer = yield* makeEffectProducerFromPubSub<TReq>(pubsub, {
|
||||
const producerOptions: CreateProducerOptions = options.requestSchema === undefined
|
||||
? { topic: options.requestTopic }
|
||||
: { topic: options.requestTopic, schema: options.requestSchema };
|
||||
const producerBackend = yield* pubsub.createProducer<TReq>(producerOptions);
|
||||
const producer = makeEffectProducerHandle<TReq>(producerBackend, {
|
||||
topic: options.requestTopic,
|
||||
...(options.requestSchema === undefined ? {} : { schema: options.requestSchema }),
|
||||
});
|
||||
|
|
@ -435,9 +439,12 @@ export const makeEffectRequestResponseFromPubSub = Effect.fn("makeEffectRequestR
|
|||
};
|
||||
const backend = yield* pubsub.createConsumer<TRes>(createOptions);
|
||||
const subscribers = new Map<string, Queue.Queue<TRes>>();
|
||||
const fiber = yield* dispatchResponseLoop(backend, options.responseTopic, subscribers, config).pipe(Effect.forkChild);
|
||||
const fiber = yield* dispatchResponseLoop(backend, options.responseTopic, subscribers, config).pipe(Effect.forkScoped);
|
||||
let stopped = false;
|
||||
|
||||
const stop = Effect.fn(`RequestResponse.stop:${options.requestTopic}`)(function* () {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
yield* Fiber.interrupt(fiber);
|
||||
yield* producer.close;
|
||||
yield* closeConsumerBackend(backend, options.responseTopic, options.subscription);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue