feat(relay): in-memory queue with consume-once semantics
This commit is contained in:
58
tools/relay/queue.test.ts
Normal file
58
tools/relay/queue.test.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { describe, it, beforeEach } from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { RelayQueue, isRole } from "./queue.ts";
|
||||||
|
|
||||||
|
describe("RelayQueue", () => {
|
||||||
|
let q: RelayQueue;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
q = new RelayQueue();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("post + read roundtrip returns the message with correct fields", () => {
|
||||||
|
q.post("dev-b", "pm", "status", "Task P4 DONE");
|
||||||
|
const msgs = q.read("pm");
|
||||||
|
assert.equal(msgs.length, 1);
|
||||||
|
assert.equal(msgs[0].from, "dev-b");
|
||||||
|
assert.equal(msgs[0].to, "pm");
|
||||||
|
assert.equal(msgs[0].kind, "status");
|
||||||
|
assert.equal(msgs[0].body, "Task P4 DONE");
|
||||||
|
assert.ok(typeof msgs[0].id === "string" && msgs[0].id.length > 0);
|
||||||
|
assert.ok(typeof msgs[0].ts === "string");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("consume-once: second read returns empty", () => {
|
||||||
|
q.post("dev-a", "pm", "question", "Should I use approach A?");
|
||||||
|
q.read("pm");
|
||||||
|
const second = q.read("pm");
|
||||||
|
assert.deepEqual(second, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("list_pending does not drain inbox", () => {
|
||||||
|
q.post("dev-b", "pm", "directive", "PROCEED");
|
||||||
|
const before = q.pending("pm");
|
||||||
|
assert.equal(before.count, 1);
|
||||||
|
const after = q.read("pm");
|
||||||
|
assert.equal(after.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("FIFO ordering across multiple senders", () => {
|
||||||
|
q.post("dev-a", "pm", "status", "first");
|
||||||
|
q.post("dev-b", "pm", "status", "second");
|
||||||
|
q.post("dev-a", "pm", "question", "third");
|
||||||
|
const msgs = q.read("pm");
|
||||||
|
assert.equal(msgs.length, 3);
|
||||||
|
assert.equal(msgs[0].body, "first");
|
||||||
|
assert.equal(msgs[1].body, "second");
|
||||||
|
assert.equal(msgs[2].body, "third");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("isRole rejects unknown strings", () => {
|
||||||
|
assert.ok(isRole("pm"));
|
||||||
|
assert.ok(isRole("dev-a"));
|
||||||
|
assert.ok(isRole("dev-b"));
|
||||||
|
assert.ok(!isRole("dev-c"));
|
||||||
|
assert.ok(!isRole(""));
|
||||||
|
assert.ok(!isRole("PM"));
|
||||||
|
});
|
||||||
|
});
|
||||||
55
tools/relay/queue.ts
Normal file
55
tools/relay/queue.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { randomUUID } from "node:crypto";
|
||||||
|
|
||||||
|
export type Role = "pm" | "dev-a" | "dev-b";
|
||||||
|
export type MessageKind = "status" | "question" | "directive" | "free";
|
||||||
|
|
||||||
|
export interface RelayMessage {
|
||||||
|
id: string;
|
||||||
|
from: Role;
|
||||||
|
to: Role;
|
||||||
|
kind: MessageKind;
|
||||||
|
body: string;
|
||||||
|
ts: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const KNOWN_ROLES = new Set<string>(["pm", "dev-a", "dev-b"]);
|
||||||
|
|
||||||
|
export function isRole(s: string): s is Role {
|
||||||
|
return KNOWN_ROLES.has(s);
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RelayQueue {
|
||||||
|
private readonly queues = new Map<Role, RelayMessage[]>([
|
||||||
|
["pm", []],
|
||||||
|
["dev-a", []],
|
||||||
|
["dev-b", []],
|
||||||
|
]);
|
||||||
|
|
||||||
|
post(from: Role, to: Role, kind: MessageKind, body: string): RelayMessage {
|
||||||
|
const msg: RelayMessage = {
|
||||||
|
id: randomUUID(),
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
kind,
|
||||||
|
body,
|
||||||
|
ts: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
this.queues.get(to)!.push(msg);
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
read(forRole: Role): RelayMessage[] {
|
||||||
|
const inbox = this.queues.get(forRole)!;
|
||||||
|
const messages = [...inbox];
|
||||||
|
inbox.length = 0;
|
||||||
|
return messages;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending(forRole: Role): { count: number; kinds: MessageKind[] } {
|
||||||
|
const inbox = this.queues.get(forRole)!;
|
||||||
|
return {
|
||||||
|
count: inbox.length,
|
||||||
|
kinds: inbox.map((m) => m.kind),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user