mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-05 19:32:11 +02:00
Remove native classes from TS runtime
This commit is contained in:
parent
952daf325d
commit
dca2786828
79 changed files with 7622 additions and 6703 deletions
|
|
@ -33,67 +33,55 @@ export interface ConsumerOptions<T> {
|
|||
rateLimitTimeoutMs?: number;
|
||||
}
|
||||
|
||||
export class Consumer<T> {
|
||||
private backend: BackendConsumer<T> | null = null;
|
||||
private running = false;
|
||||
private abortController = new AbortController();
|
||||
private readonly options: ConsumerOptions<T>;
|
||||
declare const ConsumerMessageType: unique symbol;
|
||||
|
||||
private readonly concurrency: number;
|
||||
private readonly rateLimitRetryMs: number;
|
||||
export interface Consumer<T> {
|
||||
readonly [ConsumerMessageType]?: (_: T) => T;
|
||||
readonly start: (flow: FlowContext) => Promise<void>;
|
||||
readonly stop: () => Promise<void>;
|
||||
}
|
||||
|
||||
constructor(options: ConsumerOptions<T>) {
|
||||
this.options = options;
|
||||
this.concurrency = options.concurrency ?? 1;
|
||||
this.rateLimitRetryMs = options.rateLimitRetryMs ?? 10_000;
|
||||
}
|
||||
export function makeConsumer<T>(options: ConsumerOptions<T>): Consumer<T> {
|
||||
let backend: BackendConsumer<T> | null = null;
|
||||
let running = false;
|
||||
let abortController = new AbortController();
|
||||
const concurrency = options.concurrency ?? 1;
|
||||
const rateLimitRetryMs = options.rateLimitRetryMs ?? 10_000;
|
||||
|
||||
async start(flow: FlowContext): Promise<void> {
|
||||
this.backend = await this.options.pubsub.createConsumer<T>({
|
||||
topic: this.options.topic,
|
||||
subscription: this.options.subscription,
|
||||
initialPosition: this.options.initialPosition ?? "latest",
|
||||
});
|
||||
|
||||
this.running = true;
|
||||
|
||||
// Spawn concurrent consumer tasks
|
||||
const tasks = Array.from({ length: this.concurrency }, () =>
|
||||
this.consumeLoop(flow),
|
||||
);
|
||||
// Run all concurrently — first rejection stops all
|
||||
await Promise.all(tasks);
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.running = false;
|
||||
this.abortController.abort();
|
||||
if (this.backend !== null) {
|
||||
await this.backend.close();
|
||||
this.backend = null;
|
||||
const handleWithRetry = async (msg: Message<T>, flow: FlowContext): Promise<void> => {
|
||||
try {
|
||||
await options.handler(msg.value(), msg.properties(), flow);
|
||||
} catch (err) {
|
||||
if (S.is(TooManyRequestsError)(err)) {
|
||||
console.warn(`[Consumer] Rate limited, retrying in ${rateLimitRetryMs}ms`);
|
||||
await sleep(rateLimitRetryMs);
|
||||
await options.handler(msg.value(), msg.properties(), flow);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async consumeLoop(flow: FlowContext): Promise<void> {
|
||||
while (this.running) {
|
||||
const consumeLoop = async (flow: FlowContext): Promise<void> => {
|
||||
while (running) {
|
||||
let msg: Message<T> | null = null;
|
||||
try {
|
||||
const backend = this.backend;
|
||||
if (backend === null) throw new Error("Consumer backend not started");
|
||||
const currentBackend = backend;
|
||||
if (currentBackend === null) throw new Error("Consumer backend not started");
|
||||
|
||||
msg = await backend.receive(2000);
|
||||
msg = await currentBackend.receive(2000);
|
||||
if (msg === null) continue;
|
||||
|
||||
await this.handleWithRetry(msg, flow);
|
||||
await backend.acknowledge(msg);
|
||||
await handleWithRetry(msg, flow);
|
||||
await currentBackend.acknowledge(msg);
|
||||
} catch (err) {
|
||||
if (!this.running) break;
|
||||
if (!running) break;
|
||||
console.error("[Consumer] Error in consume loop:", err);
|
||||
if (msg !== null) {
|
||||
try {
|
||||
const backend = this.backend;
|
||||
if (backend !== null) {
|
||||
await backend.negativeAcknowledge(msg);
|
||||
const currentBackend = backend;
|
||||
if (currentBackend !== null) {
|
||||
await currentBackend.negativeAcknowledge(msg);
|
||||
}
|
||||
} catch (nakErr) {
|
||||
console.error("[Consumer] Failed to nak message:", nakErr);
|
||||
|
|
@ -102,21 +90,35 @@ export class Consumer<T> {
|
|||
await sleep(1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private async handleWithRetry(msg: Message<T>, flow: FlowContext): Promise<void> {
|
||||
try {
|
||||
await this.options.handler(msg.value(), msg.properties(), flow);
|
||||
} catch (err) {
|
||||
if (S.is(TooManyRequestsError)(err)) {
|
||||
console.warn(`[Consumer] Rate limited, retrying in ${this.rateLimitRetryMs}ms`);
|
||||
await sleep(this.rateLimitRetryMs);
|
||||
await this.options.handler(msg.value(), msg.properties(), flow);
|
||||
} else {
|
||||
throw err;
|
||||
return {
|
||||
start: async (flow) => {
|
||||
backend = await options.pubsub.createConsumer<T>({
|
||||
topic: options.topic,
|
||||
subscription: options.subscription,
|
||||
initialPosition: options.initialPosition ?? "latest",
|
||||
});
|
||||
|
||||
running = true;
|
||||
|
||||
// Spawn concurrent consumer tasks.
|
||||
const tasks = Array.from({ length: concurrency }, () =>
|
||||
consumeLoop(flow),
|
||||
);
|
||||
// Run all concurrently: first rejection stops all.
|
||||
await Promise.all(tasks);
|
||||
},
|
||||
stop: async () => {
|
||||
running = false;
|
||||
abortController.abort();
|
||||
abortController = new AbortController();
|
||||
if (backend !== null) {
|
||||
await backend.close();
|
||||
backend = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function sleep(ms: number): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
export { Producer } from "./producer.js";
|
||||
export { Consumer, type MessageHandler, type FlowContext, type ConsumerOptions } from "./consumer.js";
|
||||
export { Subscriber, AsyncQueue } from "./subscriber.js";
|
||||
export { RequestResponse, type RequestResponseOptions } from "./request-response.js";
|
||||
export { makeProducer, type Producer } from "./producer.js";
|
||||
export { makeConsumer, type Consumer, type MessageHandler, type FlowContext, type ConsumerOptions } from "./consumer.js";
|
||||
export { makeAsyncQueue, makeSubscriber, type Subscriber, type AsyncQueue } from "./subscriber.js";
|
||||
export { makeRequestResponse, type RequestResponse, type RequestResponseOptions } from "./request-response.js";
|
||||
export {
|
||||
ConsumerFactory,
|
||||
ConsumerFactoryLive,
|
||||
|
|
|
|||
|
|
@ -4,47 +4,47 @@
|
|||
* Python reference: trustgraph-base/trustgraph/base/producer.py
|
||||
*/
|
||||
|
||||
import type { PubSubBackend, BackendProducer } from "../backend/types.js";
|
||||
import type { PubSubBackend } from "../backend/types.js";
|
||||
import type { ProducerMetrics } from "../metrics/prometheus.js";
|
||||
import { Effect } from "effect";
|
||||
import { makeEffectProducerHandle, type EffectProducer } from "./runtime.js";
|
||||
|
||||
export class Producer<T> {
|
||||
private backend: BackendProducer<T> | null = null;
|
||||
private effectProducer: EffectProducer<T> | null = null;
|
||||
private readonly pubsub: PubSubBackend;
|
||||
private readonly topic: string;
|
||||
private readonly metrics: ProducerMetrics | undefined;
|
||||
|
||||
constructor(pubsub: PubSubBackend, topic: string, metrics?: ProducerMetrics) {
|
||||
this.pubsub = pubsub;
|
||||
this.topic = topic;
|
||||
this.metrics = metrics;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.backend = await this.pubsub.createProducer<T>({ topic: this.topic });
|
||||
this.effectProducer = makeEffectProducerHandle(this.backend, {
|
||||
topic: this.topic,
|
||||
...(this.metrics === undefined ? {} : { metrics: this.metrics }),
|
||||
});
|
||||
}
|
||||
|
||||
async send(id: string, message: T): Promise<void> {
|
||||
if (this.effectProducer === null) throw new Error("Producer not started");
|
||||
|
||||
await Effect.runPromise(this.effectProducer.send(id, message));
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (this.effectProducer !== null) {
|
||||
await Effect.runPromise(
|
||||
this.effectProducer.flush.pipe(
|
||||
Effect.flatMap(() => this.effectProducer === null ? Effect.void : this.effectProducer.close),
|
||||
),
|
||||
);
|
||||
this.effectProducer = null;
|
||||
this.backend = null;
|
||||
}
|
||||
}
|
||||
export interface Producer<T> {
|
||||
readonly start: () => Promise<void>;
|
||||
readonly send: (id: string, message: T) => Promise<void>;
|
||||
readonly stop: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function makeProducer<T>(
|
||||
pubsub: PubSubBackend,
|
||||
topic: string,
|
||||
metrics?: ProducerMetrics,
|
||||
): Producer<T> {
|
||||
let effectProducer: EffectProducer<T> | null = null;
|
||||
|
||||
return {
|
||||
start: async () => {
|
||||
const backend = await pubsub.createProducer<T>({ topic });
|
||||
effectProducer = makeEffectProducerHandle(backend, {
|
||||
topic,
|
||||
...(metrics === undefined ? {} : { metrics }),
|
||||
});
|
||||
},
|
||||
send: async (id, message) => {
|
||||
if (effectProducer === null) throw new Error("Producer not started");
|
||||
|
||||
await Effect.runPromise(effectProducer.send(id, message));
|
||||
},
|
||||
stop: async () => {
|
||||
if (effectProducer !== null) {
|
||||
const producer = effectProducer;
|
||||
await Effect.runPromise(
|
||||
producer.flush.pipe(
|
||||
Effect.flatMap(() => producer.close),
|
||||
),
|
||||
);
|
||||
effectProducer = null;
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,8 +8,8 @@
|
|||
*/
|
||||
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { Producer } from "./producer.js";
|
||||
import { Subscriber } from "./subscriber.js";
|
||||
import { makeProducer, type Producer } from "./producer.js";
|
||||
import { makeSubscriber, type Subscriber } from "./subscriber.js";
|
||||
import type { PubSubBackend } from "../backend/types.js";
|
||||
|
||||
export interface RequestResponseOptions {
|
||||
|
|
@ -19,73 +19,76 @@ export interface RequestResponseOptions {
|
|||
subscription: string;
|
||||
}
|
||||
|
||||
export class RequestResponse<TReq, TRes> {
|
||||
private producer: Producer<TReq>;
|
||||
private subscriber: Subscriber<TRes>;
|
||||
|
||||
constructor(options: RequestResponseOptions) {
|
||||
this.producer = new Producer<TReq>(options.pubsub, options.requestTopic);
|
||||
this.subscriber = new Subscriber<TRes>(
|
||||
options.pubsub,
|
||||
options.responseTopic,
|
||||
options.subscription,
|
||||
);
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
await this.producer.start();
|
||||
await this.subscriber.start();
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
await this.producer.stop();
|
||||
await this.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.
|
||||
*/
|
||||
async request(
|
||||
export interface RequestResponse<TReq, TRes> {
|
||||
readonly start: () => Promise<void>;
|
||||
readonly stop: () => Promise<void>;
|
||||
readonly request: (
|
||||
request: TReq,
|
||||
options?: {
|
||||
timeoutMs?: number;
|
||||
recipient?: (response: TRes) => Promise<boolean>;
|
||||
},
|
||||
): Promise<TRes> {
|
||||
const id = randomUUID();
|
||||
const timeoutMs = options?.timeoutMs ?? 300_000;
|
||||
const recipient = options?.recipient;
|
||||
|
||||
const queue = this.subscriber.subscribe(id);
|
||||
|
||||
try {
|
||||
await this.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 {
|
||||
this.subscriber.unsubscribe(id);
|
||||
}
|
||||
}
|
||||
) => Promise<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,
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,114 +13,84 @@ type Resolver<T> = {
|
|||
/**
|
||||
* Simple async queue for inter-task communication (replaces asyncio.Queue).
|
||||
*/
|
||||
export class AsyncQueue<T> {
|
||||
private buffer: T[] = [];
|
||||
private waiters: Array<(value: T) => void> = [];
|
||||
|
||||
push(item: T): void {
|
||||
const waiter = this.waiters.shift();
|
||||
if (waiter !== undefined) {
|
||||
waiter(item);
|
||||
} else {
|
||||
this.buffer.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
async pop(timeoutMs?: number): Promise<T> {
|
||||
const buffered = this.buffer.shift();
|
||||
if (buffered !== undefined) return buffered;
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const waiter = (value: T) => {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
this.waiters.push(waiter);
|
||||
|
||||
if (timeoutMs !== undefined) {
|
||||
timer = setTimeout(() => {
|
||||
const idx = this.waiters.indexOf(waiter);
|
||||
if (idx !== -1) this.waiters.splice(idx, 1);
|
||||
reject(new Error(`Queue.pop timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.buffer.length;
|
||||
}
|
||||
export interface AsyncQueue<T> {
|
||||
readonly push: (item: T) => void;
|
||||
readonly pop: (timeoutMs?: number) => Promise<T>;
|
||||
readonly length: number;
|
||||
}
|
||||
|
||||
export class Subscriber<T> {
|
||||
private backend: BackendConsumer<T> | null = null;
|
||||
private running = false;
|
||||
private readonly pubsub: PubSubBackend;
|
||||
private readonly topic: string;
|
||||
private readonly subscription: string;
|
||||
export function makeAsyncQueue<T>(): AsyncQueue<T> {
|
||||
const buffer: T[] = [];
|
||||
const waiters: Array<(value: T) => void> = [];
|
||||
|
||||
return {
|
||||
push: (item) => {
|
||||
const waiter = waiters.shift();
|
||||
if (waiter !== undefined) {
|
||||
waiter(item);
|
||||
} else {
|
||||
buffer.push(item);
|
||||
}
|
||||
},
|
||||
pop: async (timeoutMs) => {
|
||||
const buffered = buffer.shift();
|
||||
if (buffered !== undefined) return buffered;
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
let timer: ReturnType<typeof setTimeout> | undefined;
|
||||
|
||||
const waiter = (value: T) => {
|
||||
if (timer !== undefined) clearTimeout(timer);
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
waiters.push(waiter);
|
||||
|
||||
if (timeoutMs !== undefined) {
|
||||
timer = setTimeout(() => {
|
||||
const idx = waiters.indexOf(waiter);
|
||||
if (idx !== -1) waiters.splice(idx, 1);
|
||||
reject(new Error(`Queue.pop timed out after ${timeoutMs}ms`));
|
||||
}, timeoutMs);
|
||||
}
|
||||
});
|
||||
},
|
||||
get length() {
|
||||
return buffer.length;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface Subscriber<T> {
|
||||
readonly start: () => Promise<void>;
|
||||
readonly stop: () => Promise<void>;
|
||||
readonly subscribe: (id: string) => AsyncQueue<T>;
|
||||
readonly subscribeAll: (id: string) => AsyncQueue<T>;
|
||||
readonly unsubscribe: (id: string) => void;
|
||||
readonly unsubscribeAll: (id: string) => void;
|
||||
}
|
||||
|
||||
export function makeSubscriber<T>(
|
||||
pubsub: PubSubBackend,
|
||||
topic: string,
|
||||
subscription: string,
|
||||
): Subscriber<T> {
|
||||
let backend: BackendConsumer<T> | null = null;
|
||||
let running = false;
|
||||
|
||||
// ID-specific subscriptions (request/response correlation)
|
||||
private idSubscribers = new Map<string, Resolver<T>>();
|
||||
const idSubscribers = new Map<string, Resolver<T>>();
|
||||
// Wildcard subscribers (receive all messages)
|
||||
private allSubscribers = new Map<string, Resolver<T>>();
|
||||
const allSubscribers = new Map<string, Resolver<T>>();
|
||||
|
||||
constructor(pubsub: PubSubBackend, topic: string, subscription: string) {
|
||||
this.pubsub = pubsub;
|
||||
this.topic = topic;
|
||||
this.subscription = subscription;
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
this.backend = await this.pubsub.createConsumer<T>({
|
||||
topic: this.topic,
|
||||
subscription: this.subscription,
|
||||
});
|
||||
this.running = true;
|
||||
// Start the dispatch loop (fire and forget — runs until stop)
|
||||
this.dispatchLoop().catch((err) => {
|
||||
if (this.running === true) console.error("[Subscriber] dispatch loop error:", err);
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
this.running = false;
|
||||
if (this.backend !== null) {
|
||||
await this.backend.close();
|
||||
this.backend = null;
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(id: string): AsyncQueue<T> {
|
||||
const queue = new AsyncQueue<T>();
|
||||
this.idSubscribers.set(id, { queue });
|
||||
return queue;
|
||||
}
|
||||
|
||||
subscribeAll(id: string): AsyncQueue<T> {
|
||||
const queue = new AsyncQueue<T>();
|
||||
this.allSubscribers.set(id, { queue });
|
||||
return queue;
|
||||
}
|
||||
|
||||
unsubscribe(id: string): void {
|
||||
this.idSubscribers.delete(id);
|
||||
}
|
||||
|
||||
unsubscribeAll(id: string): void {
|
||||
this.allSubscribers.delete(id);
|
||||
}
|
||||
|
||||
private async dispatchLoop(): Promise<void> {
|
||||
const dispatchLoop = async (): Promise<void> => {
|
||||
let consecutiveErrors = 0;
|
||||
while (this.running) {
|
||||
while (running) {
|
||||
try {
|
||||
const backend = this.backend;
|
||||
if (backend === null) throw new Error("Subscriber backend not started");
|
||||
const currentBackend = backend;
|
||||
if (currentBackend === null) throw new Error("Subscriber backend not started");
|
||||
|
||||
const msg = await backend.receive(2000);
|
||||
const msg = await currentBackend.receive(2000);
|
||||
if (msg === null) continue;
|
||||
|
||||
consecutiveErrors = 0;
|
||||
|
|
@ -131,20 +101,20 @@ export class Subscriber<T> {
|
|||
|
||||
// Route to ID-specific subscriber
|
||||
if (id !== undefined && id.length > 0) {
|
||||
const sub = this.idSubscribers.get(id);
|
||||
const sub = idSubscribers.get(id);
|
||||
if (sub !== undefined) {
|
||||
sub.queue.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
// Broadcast to all-subscribers
|
||||
for (const sub of this.allSubscribers.values()) {
|
||||
for (const sub of allSubscribers.values()) {
|
||||
sub.queue.push(value);
|
||||
}
|
||||
|
||||
await backend.acknowledge(msg);
|
||||
await currentBackend.acknowledge(msg);
|
||||
} catch (err) {
|
||||
if (!this.running) break;
|
||||
if (!running) break;
|
||||
consecutiveErrors++;
|
||||
if (consecutiveErrors <= 3) {
|
||||
console.error("[Subscriber] Error:", err);
|
||||
|
|
@ -156,5 +126,42 @@ export class Subscriber<T> {
|
|||
await new Promise((r) => setTimeout(r, delay));
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
start: async () => {
|
||||
backend = await pubsub.createConsumer<T>({
|
||||
topic,
|
||||
subscription,
|
||||
});
|
||||
running = true;
|
||||
// Start the dispatch loop (fire and forget; runs until stop).
|
||||
dispatchLoop().catch((err) => {
|
||||
if (running === true) console.error("[Subscriber] dispatch loop error:", err);
|
||||
});
|
||||
},
|
||||
stop: async () => {
|
||||
running = false;
|
||||
if (backend !== null) {
|
||||
await backend.close();
|
||||
backend = null;
|
||||
}
|
||||
},
|
||||
subscribe: (id) => {
|
||||
const queue = makeAsyncQueue<T>();
|
||||
idSubscribers.set(id, { queue });
|
||||
return queue;
|
||||
},
|
||||
subscribeAll: (id) => {
|
||||
const queue = makeAsyncQueue<T>();
|
||||
allSubscribers.set(id, { queue });
|
||||
return queue;
|
||||
},
|
||||
unsubscribe: (id) => {
|
||||
idSubscribers.delete(id);
|
||||
},
|
||||
unsubscribeAll: (id) => {
|
||||
allSubscribers.delete(id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue