kittybrowse / src /runtime /mesh.ts
SNAPKITTYWEST's picture
push from SNAPKITTYWEST/kittybrowse
39b3fb5 verified
Raw
History Blame Contribute Delete
3.21 kB
import { EventEmitter } from "node:events";
import { connect, type NatsConnection, StringCodec } from "nats";
export type MeshKind = "nats" | "local";
export type MeshHandler = (payload: string, subject: string) => void | Promise<void>;
export interface MeshBus {
readonly kind: MeshKind;
publish(subject: string, payload: string): Promise<void>;
subscribe(subject: string, handler: MeshHandler): Promise<() => Promise<void>>;
close(): Promise<void>;
}
function matchesSubject(pattern: string, subject: string): boolean {
const patternParts = pattern.split(".");
const subjectParts = subject.split(".");
for (let i = 0; i < patternParts.length; i += 1) {
const part = patternParts[i];
if (part === ">") {
return true;
}
if (part !== "*" && part !== subjectParts[i]) {
return false;
}
}
return patternParts.length === subjectParts.length;
}
export class LocalMeshBus implements MeshBus {
readonly kind = "local" as const;
private readonly bus = new EventEmitter();
private readonly subscriptions = new Map<string, Set<MeshHandler>>();
async publish(subject: string, payload: string): Promise<void> {
for (const [pattern, handlers] of this.subscriptions.entries()) {
if (matchesSubject(pattern, subject)) {
for (const handler of handlers) {
queueMicrotask(() => void handler(payload, subject));
}
}
}
this.bus.emit(subject, payload);
}
async subscribe(subject: string, handler: MeshHandler): Promise<() => Promise<void>> {
const handlers = this.subscriptions.get(subject) ?? new Set<MeshHandler>();
handlers.add(handler);
this.subscriptions.set(subject, handlers);
return async () => {
handlers.delete(handler);
if (handlers.size === 0) {
this.subscriptions.delete(subject);
}
};
}
async close(): Promise<void> {
this.subscriptions.clear();
this.bus.removeAllListeners();
}
}
export class NatsMeshBus implements MeshBus {
readonly kind = "nats" as const;
private readonly codec = StringCodec();
constructor(private readonly nc: NatsConnection) {}
async publish(subject: string, payload: string): Promise<void> {
this.nc.publish(subject, this.codec.encode(payload));
}
async subscribe(subject: string, handler: MeshHandler): Promise<() => Promise<void>> {
const subscription = this.nc.subscribe(subject);
let active = true;
void (async () => {
for await (const message of subscription) {
if (!active) {
break;
}
await handler(this.codec.decode(message.data), message.subject);
}
})();
return async () => {
active = false;
subscription.unsubscribe();
};
}
async close(): Promise<void> {
await this.nc.drain();
}
}
export async function createMeshBus(url = process.env.NATS_URL): Promise<MeshBus> {
if (!url) {
return new LocalMeshBus();
}
try {
const nc = await connect({ servers: url, timeout: 1_000 });
return new NatsMeshBus(nc);
} catch {
return new LocalMeshBus();
}
}