mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-03 23:11:00 +02:00
saving
This commit is contained in:
parent
9e9307a2aa
commit
e26caa0b12
123 changed files with 3478 additions and 10078 deletions
12
ts/packages/base/src/backend/index.ts
Normal file
12
ts/packages/base/src/backend/index.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export type {
|
||||
Message,
|
||||
BackendProducer,
|
||||
BackendConsumer,
|
||||
PubSubBackend,
|
||||
CreateProducerOptions,
|
||||
CreateConsumerOptions,
|
||||
ConsumerType,
|
||||
InitialPosition,
|
||||
} from "./types.js";
|
||||
|
||||
export { NatsBackend } from "./nats.js";
|
||||
196
ts/packages/base/src/backend/nats.ts
Normal file
196
ts/packages/base/src/backend/nats.ts
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
/**
|
||||
* NATS JetStream backend implementation.
|
||||
*
|
||||
* Replaces Pulsar as the message broker. NATS JetStream provides
|
||||
* at-least-once delivery, consumer groups, and replay — matching
|
||||
* the QoS levels used by the Python Pulsar backend.
|
||||
*
|
||||
* Python reference: trustgraph-base/trustgraph/base/pulsar_backend.py
|
||||
*/
|
||||
|
||||
import {
|
||||
connect,
|
||||
type NatsConnection,
|
||||
type JetStreamClient,
|
||||
type JetStreamManager,
|
||||
type ConsumerMessages,
|
||||
type JsMsg,
|
||||
StringCodec,
|
||||
AckPolicy,
|
||||
} from "nats";
|
||||
|
||||
import type {
|
||||
PubSubBackend,
|
||||
BackendProducer,
|
||||
BackendConsumer,
|
||||
CreateProducerOptions,
|
||||
CreateConsumerOptions,
|
||||
Message,
|
||||
} from "./types.js";
|
||||
|
||||
const sc = StringCodec();
|
||||
|
||||
class NatsMessage<T> implements Message<T> {
|
||||
constructor(
|
||||
private readonly msg: JsMsg,
|
||||
private readonly decoded: T,
|
||||
) {}
|
||||
|
||||
value(): T {
|
||||
return this.decoded;
|
||||
}
|
||||
|
||||
properties(): Record<string, string> {
|
||||
const headers = this.msg.headers;
|
||||
const props: Record<string, string> = {};
|
||||
if (headers) {
|
||||
for (const [key, values] of headers) {
|
||||
props[key] = values[0];
|
||||
}
|
||||
}
|
||||
return props;
|
||||
}
|
||||
}
|
||||
|
||||
class NatsProducer<T> implements BackendProducer<T> {
|
||||
constructor(
|
||||
private readonly js: JetStreamClient,
|
||||
private readonly subject: string,
|
||||
) {}
|
||||
|
||||
async send(message: T, properties?: Record<string, string>): Promise<void> {
|
||||
const data = sc.encode(JSON.stringify(message));
|
||||
const opts: Record<string, unknown> = {};
|
||||
|
||||
if (properties && Object.keys(properties).length > 0) {
|
||||
const { headers } = await import("nats");
|
||||
const hdrs = headers();
|
||||
for (const [key, val] of Object.entries(properties)) {
|
||||
hdrs.append(key, val);
|
||||
}
|
||||
opts.headers = hdrs;
|
||||
}
|
||||
|
||||
await this.js.publish(this.subject, data, opts);
|
||||
}
|
||||
|
||||
async flush(): Promise<void> {
|
||||
// NATS publishes are flushed on the connection level
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
// No per-producer cleanup needed for NATS
|
||||
}
|
||||
}
|
||||
|
||||
class NatsConsumer<T> implements BackendConsumer<T> {
|
||||
private messages: ConsumerMessages | null = null;
|
||||
|
||||
constructor(
|
||||
private readonly js: JetStreamClient,
|
||||
private readonly jsm: JetStreamManager,
|
||||
private readonly subject: string,
|
||||
private readonly subscription: string,
|
||||
private readonly initialPosition: "latest" | "earliest",
|
||||
) {}
|
||||
|
||||
async init(): Promise<void> {
|
||||
// Ensure stream exists
|
||||
const streamName = this.streamNameFromSubject(this.subject);
|
||||
try {
|
||||
await this.jsm.streams.info(streamName);
|
||||
} catch {
|
||||
await this.jsm.streams.add({
|
||||
name: streamName,
|
||||
subjects: [this.subject],
|
||||
});
|
||||
}
|
||||
|
||||
// Create or bind to durable consumer
|
||||
const consumer = await this.js.consumers.get(streamName, this.subscription);
|
||||
this.messages = await consumer.consume();
|
||||
}
|
||||
|
||||
async receive(timeoutMs = 2000): Promise<Message<T> | null> {
|
||||
if (!this.messages) throw new Error("Consumer not initialized");
|
||||
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
for await (const msg of this.messages) {
|
||||
const decoded = JSON.parse(sc.decode(msg.data)) as T;
|
||||
return new NatsMessage(msg, decoded);
|
||||
}
|
||||
|
||||
if (Date.now() >= deadline) return null;
|
||||
return null;
|
||||
}
|
||||
|
||||
async acknowledge(message: Message<T>): Promise<void> {
|
||||
const natsMsg = message as NatsMessage<T>;
|
||||
// Access internal JsMsg for ack — in practice we'd store the ref
|
||||
// This is a simplified version; real impl tracks msg refs
|
||||
void natsMsg;
|
||||
}
|
||||
|
||||
async negativeAcknowledge(message: Message<T>): Promise<void> {
|
||||
void message;
|
||||
}
|
||||
|
||||
async unsubscribe(): Promise<void> {
|
||||
// Drain and close consumer
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.messages) {
|
||||
this.messages.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private streamNameFromSubject(subject: string): string {
|
||||
// Convert topic like "tg.flow.text-completion" to stream name "tg_flow"
|
||||
const parts = subject.split(".");
|
||||
return parts.slice(0, 2).join("_");
|
||||
}
|
||||
}
|
||||
|
||||
export class NatsBackend implements PubSubBackend {
|
||||
private connection: NatsConnection | null = null;
|
||||
private js: JetStreamClient | null = null;
|
||||
private jsm: JetStreamManager | null = null;
|
||||
|
||||
constructor(private readonly url: string = "nats://localhost:4222") {}
|
||||
|
||||
private async ensureConnected(): Promise<void> {
|
||||
if (!this.connection) {
|
||||
this.connection = await connect({ servers: this.url });
|
||||
this.js = this.connection.jetstream();
|
||||
this.jsm = await this.connection.jetstreamManager();
|
||||
}
|
||||
}
|
||||
|
||||
async createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>> {
|
||||
await this.ensureConnected();
|
||||
return new NatsProducer<T>(this.js!, options.topic);
|
||||
}
|
||||
|
||||
async createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>> {
|
||||
await this.ensureConnected();
|
||||
const consumer = new NatsConsumer<T>(
|
||||
this.js!,
|
||||
this.jsm!,
|
||||
options.topic,
|
||||
options.subscription,
|
||||
options.initialPosition ?? "latest",
|
||||
);
|
||||
await consumer.init();
|
||||
return consumer;
|
||||
}
|
||||
|
||||
async close(): Promise<void> {
|
||||
if (this.connection) {
|
||||
await this.connection.drain();
|
||||
this.connection = null;
|
||||
this.js = null;
|
||||
this.jsm = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
45
ts/packages/base/src/backend/types.ts
Normal file
45
ts/packages/base/src/backend/types.ts
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/**
|
||||
* Core pub/sub backend abstraction.
|
||||
*
|
||||
* Mirrors Python's backend.py Protocol classes. Any message broker
|
||||
* (NATS, Pulsar, Redis Streams) implements these interfaces.
|
||||
*/
|
||||
|
||||
export interface Message<T = unknown> {
|
||||
value(): T;
|
||||
properties(): Record<string, string>;
|
||||
}
|
||||
|
||||
export interface BackendProducer<T = unknown> {
|
||||
send(message: T, properties?: Record<string, string>): Promise<void>;
|
||||
flush(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface BackendConsumer<T = unknown> {
|
||||
receive(timeoutMs?: number): Promise<Message<T> | null>;
|
||||
acknowledge(message: Message<T>): Promise<void>;
|
||||
negativeAcknowledge(message: Message<T>): Promise<void>;
|
||||
unsubscribe(): Promise<void>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
|
||||
export type ConsumerType = "shared" | "exclusive" | "failover";
|
||||
export type InitialPosition = "latest" | "earliest";
|
||||
|
||||
export interface CreateProducerOptions {
|
||||
topic: string;
|
||||
}
|
||||
|
||||
export interface CreateConsumerOptions {
|
||||
topic: string;
|
||||
subscription: string;
|
||||
initialPosition?: InitialPosition;
|
||||
consumerType?: ConsumerType;
|
||||
}
|
||||
|
||||
export interface PubSubBackend {
|
||||
createProducer<T>(options: CreateProducerOptions): Promise<BackendProducer<T>>;
|
||||
createConsumer<T>(options: CreateConsumerOptions): Promise<BackendConsumer<T>>;
|
||||
close(): Promise<void>;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue