The old bus's wire is pinned unchanged, because this has to merge to a running mesh
Every module's event names were converted from the old bus's routing keys to local names, and this client maps them back. If that mapping is wrong anywhere a live mesh's events stop being delivered — silently, because a binding that matches nothing is not an error. So the mapping is pinned against the literal routing keys the mesh published before, taken from the manifests as they were: what each module now emits, what each now binds, and that a handler still matches what the bus delivers. Including the audit logger's "everything", which must stay `#` on this bus. And a key already in the old form is left alone, so a module built from an older manifest keeps working beside one built from a current manifest — which is the state the mesh will actually be in between deployments.
This commit is contained in:
+4
-4
@@ -373,7 +373,7 @@ function toEnvelope<T>(msg: amqp.ConsumeMessage): Envelope<T> {
|
|||||||
|
|
||||||
/** AMQP topic matching: `*` matches one word, `#` zero or more. Used to fan a shared queue's
|
/** AMQP topic matching: `*` matches one word, `#` zero or more. Used to fan a shared queue's
|
||||||
* deliveries out to the handlers whose pattern actually matches the routing key. */
|
* deliveries out to the handlers whose pattern actually matches the routing key. */
|
||||||
function topicMatches(pattern: string, key: string): boolean {
|
export function topicMatches(pattern: string, key: string): boolean {
|
||||||
return matchFrom(pattern.split("."), 0, key.split("."), 0);
|
return matchFrom(pattern.split("."), 0, key.split("."), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,19 +404,19 @@ function matchFrom(p: string[], pi: number, k: string[], ki: number): boolean {
|
|||||||
* **Why it exists at all.** Until 04-ISSUES/127 every module passed the routing key itself, which
|
* **Why it exists at all.** Until 04-ISSUES/127 every module passed the routing key itself, which
|
||||||
* worked on this bus and derived into a namespace nobody owns on the one being built. Converting the
|
* worked on this bus and derived into a namespace nobody owns on the one being built. Converting the
|
||||||
* modules to local names without this would have broken the mesh that is actually running. */
|
* modules to local names without this would have broken the mesh that is actually running. */
|
||||||
function routingKeyFor(key: string, self: string): string {
|
export function routingKeyFor(key: string, self: string): string {
|
||||||
return key.startsWith("module.") ? key : `module.${self}.${key}`;
|
return key.startsWith("module.") ? key : `module.${self}.${key}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A local pattern as this bus's binding. `**` is the mesh's wildcard for the rest of a name; here
|
/** A local pattern as this bus's binding. `**` is the mesh's wildcard for the rest of a name; here
|
||||||
* that is `#`, and on the bus being built it is `>`. Neither spelling appears in a manifest. */
|
* that is `#`, and on the bus being built it is `>`. Neither spelling appears in a manifest. */
|
||||||
function bindingFor(pattern: string): string {
|
export function bindingFor(pattern: string): string {
|
||||||
const here = pattern.split(".").map((part) => (part === "**" ? "#" : part)).join(".");
|
const here = pattern.split(".").map((part) => (part === "**" ? "#" : part)).join(".");
|
||||||
if (here === "#") return "#";
|
if (here === "#") return "#";
|
||||||
return here.startsWith("module.") ? here : `module.${here}`;
|
return here.startsWith("module.") ? here : `module.${here}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** A routing key as the local name a handler and a manifest both use: the emitter and the event. */
|
/** A routing key as the local name a handler and a manifest both use: the emitter and the event. */
|
||||||
function localKeyFor(routingKey: string): string {
|
export function localKeyFor(routingKey: string): string {
|
||||||
return routingKey.startsWith("module.") ? routingKey.slice("module.".length) : routingKey;
|
return routingKey.startsWith("module.") ? routingKey.slice("module.".length) : routingKey;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/**
|
||||||
|
* **The old bus's wire is byte-identical after the rename, and this is the test that lets the change
|
||||||
|
* be merged to a running mesh.**
|
||||||
|
*
|
||||||
|
* Every module's event names were converted from the old bus's routing keys to local names
|
||||||
|
* (novox/hq 04-ISSUES/127), and the old bus's client maps them back. If that mapping is wrong
|
||||||
|
* anywhere, a live mesh's events stop being delivered — silently, because a binding that matches
|
||||||
|
* nothing is not an error.
|
||||||
|
*
|
||||||
|
* So this pins the mapping against the literal routing keys the mesh used before, taken from the
|
||||||
|
* manifests as they were. It needs no bus: it is about a string.
|
||||||
|
*/
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
|
||||||
|
import { routingKeyFor, bindingFor, localKeyFor, topicMatches } from "../dist/broker-amqp.js";
|
||||||
|
|
||||||
|
test("a converted emit produces the routing key the mesh published before", () => {
|
||||||
|
// left: what the module's code says now. right: what went on the wire before, unchanged.
|
||||||
|
const same: [string, string, string][] = [
|
||||||
|
["plex", "playback.started", "module.plex.playback.started"],
|
||||||
|
["sonarr", "download.completed", "module.sonarr.download.completed"],
|
||||||
|
["builder", "built", "module.builder.built"],
|
||||||
|
["mesh-catalog", "upgraded", "module.mesh-catalog.upgraded"],
|
||||||
|
["keycloak", "user.created", "module.keycloak.user.created"],
|
||||||
|
["mesh-vault", "secret.rotated", "module.mesh-vault.secret.rotated"],
|
||||||
|
];
|
||||||
|
for (const [self, local, before] of same) {
|
||||||
|
assert.equal(routingKeyFor(local, self), before, `${self} emitting ${local}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a converted subscription binds what it bound before", () => {
|
||||||
|
const same: [string, string][] = [
|
||||||
|
["builder.built", "module.builder.built"],
|
||||||
|
["*.download.completed", "module.*.download.completed"],
|
||||||
|
["*.usage.*", "module.*.usage.*"],
|
||||||
|
// The audit logger's "everything": `#` on this bus, and it must stay `#`.
|
||||||
|
["**", "#"],
|
||||||
|
];
|
||||||
|
for (const [declared, before] of same) {
|
||||||
|
assert.equal(bindingFor(declared), before, `consuming ${declared}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a handler still matches what the bus delivers", () => {
|
||||||
|
// The key a handler is given is the local one now, and the pattern it compares against is local
|
||||||
|
// too — so the pair must still meet for every case the mesh actually has.
|
||||||
|
const pairs: [string, string][] = [
|
||||||
|
["builder.built", "module.builder.built"],
|
||||||
|
["*.download.completed", "module.sonarr.download.completed"],
|
||||||
|
["*.usage.*", "module.anthropic-consumer.usage.session"],
|
||||||
|
["**", "module.anything.at.all"],
|
||||||
|
];
|
||||||
|
for (const [pattern, delivered] of pairs) {
|
||||||
|
assert.ok(
|
||||||
|
topicMatches(pattern, localKeyFor(delivered)),
|
||||||
|
`${pattern} no longer matches ${delivered}, so a running module would stop reacting`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("a routing key already in the old form is left alone", () => {
|
||||||
|
// Belt for the transition: anything not yet converted still goes out as it did, so a module built
|
||||||
|
// from an older manifest keeps working beside one built from a current manifest.
|
||||||
|
assert.equal(routingKeyFor("module.plex.playback.started", "plex"), "module.plex.playback.started");
|
||||||
|
assert.equal(bindingFor("module.builder.built"), "module.builder.built");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user