The per-module containers still run this runtime; their tools and the seats they hold (the store's, the catalogue's) must be found by the console the same way as the node runtime's. It answers $SRV.PING, $SRV.INFO and $SRV.STATS with one service per process, one endpoint per tool per subject and per seat verb served, the metadata as the Go runtime writes it.
70 lines
3.4 KiB
TypeScript
70 lines
3.4 KiB
TypeScript
/**
|
|
* What a runtime serves, it announces (novox/hq ADR 0197): the TypeScript runtime the per-module
|
|
* containers still run answers the NATS services protocol's discovery in the same shape as the Go
|
|
* tool runtime — its module's tools on every subject issued, and the seat verbs it serves.
|
|
*
|
|
* MESH_TEST_NATS=nats://127.0.0.1:14232 node --test --experimental-strip-types test/announce.test.ts
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
import { connect, StringCodec } from "nats";
|
|
|
|
import { resetTools } from "@novox/mesh-sdk/tools";
|
|
|
|
import { connectNats, membershipSubject } from "../dist/broker-nats.js";
|
|
import { runTools } from "../dist/runtime.js";
|
|
|
|
const url = process.env.MESH_TEST_NATS;
|
|
const fixture = (name: string) => fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url));
|
|
const sc = StringCodec();
|
|
|
|
test("the runtime answers $SRV.INFO with what it serves, in the services protocol's format", async (t) => {
|
|
if (!url) return t.skip("MESH_TEST_NATS unset");
|
|
resetTools();
|
|
const nc = await connect({ servers: url });
|
|
const jsm = await nc.jetstreamManager();
|
|
try {
|
|
await jsm.streams.delete("ASSIGNMENTS");
|
|
} catch {
|
|
// none yet
|
|
}
|
|
await jsm.streams.add({ name: "ASSIGNMENTS", subjects: ["mesh.assignment.>"], max_msgs_per_subject: 1, allow_direct: true } as never);
|
|
await nc.jetstream().publish(membershipSubject("anchor", "shop"), sc.encode(JSON.stringify({
|
|
node: "anchor", module: "shop",
|
|
serves: [{ subject: "mesh.mod.shop.tool.{tool}.anchor" }, { subject: "mesh.mod.shop.tool.{tool}", queue: "serve.shop" }],
|
|
emits: "mesh.mod.shop.event.{event}", tools: "mesh.mod.shop.tool.tools",
|
|
})));
|
|
const shop = await connectNats({ url, node: "anchor", module: "shop" });
|
|
let stop = () => {};
|
|
try {
|
|
stop = await runTools({ broker: shop, moduleEntrypoints: [fixture("shop-tools.mjs")] });
|
|
const msg = await nc.request("$SRV.INFO.shop.anchor", sc.encode(""), { timeout: 2000 });
|
|
const info = JSON.parse(sc.decode(msg.data)) as {
|
|
type: string; name: string; id: string; version: string;
|
|
endpoints: { name: string; subject: string; queue_group: string; metadata: Record<string, string> }[];
|
|
};
|
|
assert.equal(info.type, "io.nats.micro.v1.info_response");
|
|
assert.equal(info.name, "shop");
|
|
assert.equal(info.id, "anchor");
|
|
assert.ok(info.version);
|
|
const price = info.endpoints.filter((e) => e.metadata.tool === "price").map((e) => `${e.subject}|${e.queue_group}`).sort();
|
|
assert.deepEqual(price, ["mesh.mod.shop.tool.price.anchor|", "mesh.mod.shop.tool.price|serve.shop"]);
|
|
const one = info.endpoints.find((e) => e.metadata.tool === "price")!;
|
|
assert.equal(one.metadata.kind, "tool");
|
|
assert.equal(one.metadata.module, "shop");
|
|
assert.equal(one.metadata.node, "anchor");
|
|
assert.equal(one.metadata.interchangeable, "true");
|
|
assert.ok(JSON.parse(one.metadata.schema).type === "object");
|
|
// Ping answers with the same identity; another service's request is not answered.
|
|
const ping = JSON.parse(sc.decode((await nc.request("$SRV.PING", sc.encode(""), { timeout: 2000 })).data));
|
|
assert.equal(ping.type, "io.nats.micro.v1.ping_response");
|
|
await assert.rejects(nc.request("$SRV.INFO.somebody-else", sc.encode(""), { timeout: 300 }));
|
|
} finally {
|
|
stop();
|
|
await shop.close();
|
|
await nc.close();
|
|
resetTools();
|
|
}
|
|
});
|