Tracebus
A publish/subscribe bus with two rules most buses skip: every legal path is declared up front, and every emission is written to an append-only ledger you can replay by trace id. A packet can only reach a subscriber the routing table permits — an unrouted packet is refused, not silently dropped — and a subscriber that throws is caught, recorded, and stepped over so one bad listener can never take the bus down. Thread one traceId through a chain and read the whole journey back out of the ledger, hop by hop.
The honest edge
It enforces the topology you declare and records every hop; it does NOT invent routes for you (an unrouted packet is a fault, by design) and its ids are v4-shaped for correlation, not cryptographic (Math.random). request/response is opt-in per bus via { requestResponse: true } — the bus name carries no special meaning.
Run it
node tracebus.js --demo
test_tracebus.js (72/72, mutation-bitten)
Zero-dependency JavaScript, Node or browser
The code — every file that ships
tracebus.js475 lineson GitHub →
/*
* tracebus — a traced, routing-enforced message bus.
*
* A publish/subscribe bus where every legal path is declared up front and
* every emission is written to an append-only ledger you can replay by
* trace id. A packet can only reach a subscriber the routing table permits;
* an unrouted packet is refused, not silently dropped. Nothing is delivered
* without leaving a receipt.
*
* The shape: packet ──emit(source)──▶ routingTable.resolve ──▶ subscribers
* │ │
* └────────▶ ledger.record ◀─────┘
*
* Zero dependencies. Runs in Node or the browser. No I/O, no globals, no
* clock you can't control (timestamps are ISO strings; ids are v4-shaped).
*
* Properties this file holds (see test_tracebus.js):
* 1. No delivery without a receipt. Every emit() — delivered, partial,
* rejected, or errored — appends exactly one ledger record.
* 2. No path that wasn't declared. resolve() throws on an unrouted
* (packetType, source) pair; emit() records the rejection and rethrows.
* 3. A wrong-bus packet is refused. A packet routed to bus A cannot be
* emitted on bus B, even if a subscriber is listening.
* 4. A subscriber that throws cannot break the emitter. Handler errors are
* caught, recorded against that destination, and delivery continues.
* 5. Routing entries are frozen. A registered route cannot be mutated
* after the fact (deepFreeze), so the declared topology stays declared.
* 6. A trace is reconstructable. queryByTraceId returns every hop a
* packet took, in emission order.
*
* Origin: stripped from a shipped order-management constellation's internal
* message fabric (the "one bus, many loops, every packet traced" core) and
* generalized — the request/response capability, which was hardwired to a
* single bus name in the original, is now a per-bus flag.
*
* MIT licensed. Use it for anything.
*/
'use strict';
// ────────────────────────────────────────────────────────────────────
// Primitives (inlined so the file has no dependencies).
// ────────────────────────────────────────────────────────────────────
/**
* A v4-shaped identifier: 8-4-4-4-12 hex with the version nibble pinned
* to 4 and the variant nibble in {8,9,a,b}. Uses Math.random — this is an
* identifier for correlating log records, NOT a cryptographic token.
*/
function generateId() {
const hex = '0123456789abcdef';
let id = '';
for (let i = 0; i < 36; i++) {
if (i === 8 || i === 13 || i === 18 || i === 23) {
id += '-';
} else if (i === 14) {
id += '4';
} else if (i === 19) {
id += hex[(Math.floor(Math.random() * 16) & 0x3) | 0x8];
} else {
id += hex[Math.floor(Math.random() * 16)];
}
}
return id;
}
/** Current time as an ISO-8601 string. The one place time enters. */
function now() {
return new Date().toISOString();
}
/**
* Recursively freeze an object and everything it owns. A registered
* routing entry is frozen so the declared topology cannot be edited
* after registration.
*/
function deepFreeze(obj) {
Object.freeze(obj);
for (const key of Object.keys(obj)) {
const val = obj[key];
if (val && typeof val === 'object' && !Object.isFrozen(val)) {
deepFreeze(val);
}
}
return obj;
}
// ────────────────────────────────────────────────────────────────────
// Packet — the traceable envelope.
// ────────────────────────────────────────────────────────────────────
/**
* Create a packet: a typed payload wrapped in a header that carries a
* unique packetId, a traceId (shared across a causal chain — pass it in
* opts to thread a conversation), a timestamp, and an optional source.
*
* @param {string} packetType - a non-empty type string, e.g. 'user.created'
* @param {object} payload - the packet's data (must be an object)
* @param {object} [opts] - { traceId?, source? } overrides
* @returns {object} { packetId, packetType, timestamp, traceId, source, payload }
*/
function createPacket(packetType, payload, opts) {
if (!packetType || typeof packetType !== 'string') {
throw new Error('[tracebus] Cannot create packet: packetType is required and must be a non-empty string');
}
if (!payload || typeof payload !== 'object') {
throw new Error(`[tracebus] Cannot create packet ${packetType}: payload must be an object`);
}
return {
packetId: generateId(),
packetType: packetType,
timestamp: now(),
traceId: (opts && opts.traceId) || generateId(),
source: (opts && opts.source) || null,
payload: payload
};
}
// ────────────────────────────────────────────────────────────────────
// Ledger — the append-only receipt log.
// ────────────────────────────────────────────────────────────────────
/**
* Create an append-only ledger. Every bus emission is recorded here.
* Nothing is ever removed; queries are pure reads.
*/
function createLedger() {
const entries = [];
return {
/**
* Record one emission.
* @param {object} record - { bus, packet, source, destinations?, status? }
*/
record(record) {
if (!record || !record.packet || !record.packet.packetId) {
throw new Error('[tracebus] Cannot record: a record needs a packet with a packetId');
}
entries.push({
ledgerId: generateId(),
recordedAt: now(),
bus: record.bus,
packetId: record.packet.packetId,
packetType: record.packet.packetType,
traceId: record.packet.traceId,
source: record.source,
destinations: record.destinations || [],
status: record.status || 'delivered'
});
},
/**
* Every record for one traceId, in emission order — a packet's journey.
* @param {string} traceId
* @returns {object[]}
*/
queryByTraceId(traceId) {
return entries.filter(e => e.traceId === traceId);
},
/**
* Every record of one packetType.
* @param {string} packetType
* @returns {object[]}
*/
queryByPacketType(packetType) {
return entries.filter(e => e.packetType === packetType);
},
/** Total records (diagnostic). */
count() {
return entries.length;
},
/** A copy of every record (diagnostic — the ledger stays private). */
all() {
return entries.slice();
}
};
}
// ────────────────────────────────────────────────────────────────────
// Routing table — the declared topology.
// ────────────────────────────────────────────────────────────────────
/**
* Create a routing table. Every legal (packetType, source) → destinations
* path must be registered before a packet can travel it. Registered entries
* are frozen.
*/
function createRoutingTable() {
// key = `${packetType}:${source}` → array of frozen entries
const routes = new Map();
return {
/**
* Register a legal path.
* @param {object} entry - { packetType, source, bus, destinations[],
* entryNumber?, mode?, priority? }
*/
register(entry) {
if (!entry || !entry.packetType || !entry.source || !entry.bus) {
throw new Error('[tracebus] Cannot register route: packetType, source, and bus are required');
}
if (!entry.destinations || !Array.isArray(entry.destinations) || entry.destinations.length === 0) {
throw new Error(`[tracebus] Cannot register route ${entry.packetType} from ${entry.source}: destinations must be a non-empty array`);
}
const key = `${entry.packetType}:${entry.source}`;
if (!routes.has(key)) {
routes.set(key, []);
}
routes.get(key).push(deepFreeze({
entryNumber: entry.entryNumber !== undefined ? entry.entryNumber : null,
packetType: entry.packetType,
source: entry.source,
bus: entry.bus,
destinations: entry.destinations.slice(),
mode: entry.mode || 'push',
priority: entry.priority || 'standard'
}));
},
/**
* Resolve the routes for a (packetType, source) pair.
* @throws if no route is registered — an unrouted packet is a fault.
* @returns {object[]} matching frozen entries
*/
resolve(packetType, source) {
const key = `${packetType}:${source}`;
const matched = routes.get(key);
if (!matched || matched.length === 0) {
throw new Error(`[tracebus] Unrouted packet: ${packetType} from ${source}. No matching routing entry.`);
}
return matched;
},
/** Non-throwing existence check. */
has(packetType, source) {
const key = `${packetType}:${source}`;
const matched = routes.get(key);
return !!(matched && matched.length > 0);
},
/** Total registered entries (diagnostic). */
count() {
let total = 0;
for (const list of routes.values()) {
total += list.length;
}
return total;
}
};
}
// ────────────────────────────────────────────────────────────────────
// Bus — publish/subscribe with enforced routing and a receipt per emit.
// ────────────────────────────────────────────────────────────────────
/**
* Create a bus.
* @param {string} name - this bus's name (matched against a route's `bus`)
* @param {object} routingTable - a shared routing table (createRoutingTable())
* @param {object} ledger - a shared ledger (createLedger())
* @param {object} [opts] - { requestResponse?: boolean } — enable request()
* on this bus. Default false.
*/
function createBus(name, routingTable, ledger, opts) {
if (!name || typeof name !== 'string') {
throw new Error('[tracebus] Cannot create bus: name is required and must be a non-empty string');
}
if (!routingTable || typeof routingTable.resolve !== 'function') {
throw new Error(`[tracebus] Cannot create bus ${name}: a routing table is required`);
}
if (!ledger || typeof ledger.record !== 'function') {
throw new Error(`[tracebus] Cannot create bus ${name}: a ledger is required`);
}
const requestResponse = !!(opts && opts.requestResponse);
const subscribers = new Map(); // destination name → handler
return {
name: name,
/**
* Subscribe a named destination to this bus.
* @param {string} destination - the name routing entries deliver to
* @param {function} handler - receives (packet)
*/
subscribe(destination, handler) {
if (!destination || typeof destination !== 'string') {
throw new Error(`[tracebus:${name}] Cannot subscribe: destination must be a non-empty string`);
}
if (typeof handler !== 'function') {
throw new Error(`[tracebus:${name}] Cannot subscribe ${destination}: handler must be a function`);
}
subscribers.set(destination, handler);
},
/**
* Emit a packet from a source. Routing decides destinations; a receipt
* is always recorded. Returns { delivered, missing } — destinations that
* received the packet and declared destinations with no live subscriber.
* @param {string} source - the emitter
* @param {object} packet - from createPacket()
*/
emit(source, packet) {
if (!packet || !packet.packetType) {
throw new Error(`[tracebus:${name}] Cannot emit from ${source}: invalid packet (missing packetType)`);
}
let routeEntries;
try {
routeEntries = routingTable.resolve(packet.packetType, source);
} catch (err) {
ledger.record({ bus: name, packet, source, destinations: [], status: 'rejected:unrouted' });
throw err;
}
const entry = routeEntries[0];
if (entry.bus !== name) {
ledger.record({ bus: name, packet, source, destinations: [], status: 'rejected:wrong_bus' });
throw new Error(`[tracebus:${name}] Cannot emit ${packet.packetType} from ${source}: packet is routed to bus ${entry.bus}, not ${name}`);
}
const delivered = [];
const missing = [];
for (const re of routeEntries) {
for (const dest of re.destinations) {
const handler = subscribers.get(dest);
if (handler) {
try {
handler(packet);
delivered.push(dest);
} catch (handlerErr) {
ledger.record({
bus: name, packet, source,
destinations: [dest],
status: `error:subscriber:${handlerErr.message}`
});
delivered.push(dest); // a thrown handler still received the packet
}
} else {
missing.push(dest);
}
}
}
ledger.record({
bus: name, packet, source,
destinations: delivered,
status: missing.length > 0
? `delivered:partial (missing: ${missing.join(', ')})`
: 'delivered'
});
return { delivered, missing };
},
/**
* Request/response: emit to a single destination and return its handler's
* return value. Only available when the bus was created with
* { requestResponse: true }.
* @param {string} source
* @param {object} packet
* @returns {*} the handler's return value
*/
request(source, packet) {
if (!requestResponse) {
throw new Error(`[tracebus:${name}] request() is not enabled on this bus (create it with { requestResponse: true })`);
}
if (!packet || !packet.packetType) {
throw new Error(`[tracebus:${name}] Cannot request from ${source}: invalid packet`);
}
let routeEntries;
try {
routeEntries = routingTable.resolve(packet.packetType, source);
} catch (err) {
ledger.record({ bus: name, packet, source, destinations: [], status: 'rejected:unrouted' });
throw err;
}
const entry = routeEntries[0];
if (entry.bus !== name) {
ledger.record({ bus: name, packet, source, destinations: [], status: 'rejected:wrong_bus' });
throw new Error(`[tracebus:${name}] Cannot request ${packet.packetType}: routed to bus ${entry.bus}`);
}
const dest = entry.destinations[0];
const handler = subscribers.get(dest);
if (!handler) {
ledger.record({ bus: name, packet, source, destinations: [dest], status: 'error:destination_not_registered' });
throw new Error(`[tracebus:${name}] Cannot request ${packet.packetType} from ${source}: destination ${dest} is not registered`);
}
let result;
try {
result = handler(packet);
} catch (handlerErr) {
ledger.record({ bus: name, packet, source, destinations: [dest], status: `error:handler:${handlerErr.message}` });
throw handlerErr;
}
ledger.record({ bus: name, packet, source, destinations: [dest], status: 'delivered:request_response' });
return result;
},
/** Is a destination subscribed? (diagnostic) */
hasSubscriber(destination) {
return subscribers.has(destination);
},
/** Subscriber count (diagnostic). */
subscriberCount() {
return subscribers.size;
}
};
}
// ────────────────────────────────────────────────────────────────────
// Exports — CommonJS for Node, global for the browser.
// ────────────────────────────────────────────────────────────────────
const tracebus = {
generateId,
createPacket,
createLedger,
createRoutingTable,
createBus,
deepFreeze
};
if (typeof module !== 'undefined' && module.exports) {
module.exports = tracebus;
}
if (typeof window !== 'undefined') {
window.tracebus = tracebus;
}
// ────────────────────────────────────────────────────────────────────
// Runnable demo: node tracebus.js --demo
// Wires a tiny two-subscriber bus, emits one packet, and prints the
// ledger — so `run` shows the receipt trail with no code to write.
// ────────────────────────────────────────────────────────────────────
if (typeof require !== 'undefined' && typeof module !== 'undefined' && require.main === module) {
var arg = process.argv[2];
if (arg === '--demo' || arg === undefined) {
var routing = createRoutingTable();
var ledger = createLedger();
var bus = createBus('DATA', routing, ledger);
routing.register({ packetType: 'order.placed', source: 'checkout', bus: 'DATA', destinations: ['fulfillment', 'email'] });
bus.subscribe('fulfillment', function (p) { console.log(' fulfillment got order', p.payload.id); });
bus.subscribe('email', function (p) { console.log(' email got order', p.payload.id); });
console.log('emit order.placed from checkout:');
var result = bus.emit('checkout', createPacket('order.placed', { id: 1001 }, { source: 'checkout' }));
console.log(' delivered:', result.delivered.join(', '), '| missing:', result.missing.join(', ') || '(none)');
console.log('\nledger (the receipt trail):');
ledger.all().forEach(function (e) {
console.log(' ' + e.packetType + ' bus=' + e.bus + ' status=' + e.status + ' ->[' + e.destinations.join(', ') + ']');
});
console.log('\ntry an unrouted packet (refused, but still recorded):');
try { bus.emit('checkout', createPacket('order.cancelled', { id: 1001 })); }
catch (e) { console.log(' refused: ' + e.message); }
console.log(' ledger now has ' + ledger.count() + ' records — nothing moved without a receipt.');
} else {
console.log('usage: node tracebus.js --demo');
}
}
test_tracebus.js268 lineson GitHub →
#!/usr/bin/env node
/* test_tracebus.js — proves the bus is honest: nothing moves without a receipt,
no path travels that wasn't declared, and a bad subscriber can't take the bus down.
The six properties that ARE the tool:
1. No delivery without a receipt — every emit() appends exactly one ledger record.
2. No undeclared path — an unrouted packet is refused (and the refusal is recorded).
3. A wrong-bus packet is refused — routed to A, cannot be emitted on B.
4. A throwing subscriber can't break the emitter — error caught, recorded, delivery continues.
5. Registered routes are frozen — the declared topology stays declared.
6. A trace is reconstructable — queryByTraceId returns every hop in order.
Plus the generalizations this strip added over its origin (tested hardest, per the
"a ported core gets its OWN validation" rule): request() gated by a per-bus flag,
not a hardcoded bus name. Ends with a mutation bite so a vacuous green fails loud.
stdlib only, no dependencies. Exit 0 = all pass, exit 1 = a failure (loud). */
"use strict";
var tb = require("./tracebus.js");
var pass = 0, fail = 0;
function ok(name, cond) {
if (cond) { pass++; } else { fail++; console.error("FAIL " + name); }
}
function eq(name, got, want) {
var g = JSON.stringify(got), w = JSON.stringify(want);
if (g === w) { pass++; }
else { fail++; console.error("FAIL " + name + "\n got: " + g + "\n want: " + w); }
}
function threws(fn) { try { fn(); return false; } catch (e) { return true; } }
// A fresh, wired-up bus system for a test to use.
function fixture(busOpts) {
var routing = tb.createRoutingTable();
var ledger = tb.createLedger();
var bus = tb.createBus("DATA", routing, ledger, busOpts);
return { routing: routing, ledger: ledger, bus: bus };
}
// ── createPacket: shape + validation ─────────────────────────────────────────
(function () {
var p = tb.createPacket("user.created", { id: 7 });
ok("packet has packetId", typeof p.packetId === "string" && p.packetId.length === 36);
ok("packet has traceId", typeof p.traceId === "string" && p.traceId.length === 36);
ok("packet keeps type", p.packetType === "user.created");
ok("packet keeps payload", p.payload.id === 7);
ok("packet source defaults null", p.source === null);
ok("packetId !== traceId by default", p.packetId !== p.traceId);
var threaded = tb.createPacket("user.updated", { id: 7 }, { traceId: "trace-abc", source: "svc" });
ok("traceId threads through opts", threaded.traceId === "trace-abc");
ok("source threads through opts", threaded.source === "svc");
ok("empty packetType throws", threws(function () { tb.createPacket("", {}); }));
ok("non-string packetType throws", threws(function () { tb.createPacket(42, {}); }));
ok("null payload throws", threws(function () { tb.createPacket("t", null); }));
ok("non-object payload throws", threws(function () { tb.createPacket("t", "nope"); }));
})();
// ── generateId: v4 shape + uniqueness ────────────────────────────────────────
(function () {
var a = tb.generateId(), b = tb.generateId();
ok("id is 36 chars", a.length === 36);
ok("id version nibble is 4", a[14] === "4");
ok("id variant nibble in 8,9,a,b", /[89ab]/.test(a[19]));
ok("dashes at 8,13,18,23", a[8] === "-" && a[13] === "-" && a[18] === "-" && a[23] === "-");
ok("ids are unique", a !== b);
// 500 draws, no collision
var seen = {}, collision = false;
for (var i = 0; i < 500; i++) { var id = tb.generateId(); if (seen[id]) collision = true; seen[id] = 1; }
ok("500 ids, no collision", !collision);
})();
// ── PROPERTY 1: no delivery without a receipt ────────────────────────────────
(function () {
var f = fixture();
f.routing.register({ packetType: "e", source: "src", bus: "DATA", destinations: ["sink"] });
f.bus.subscribe("sink", function () {});
ok("ledger starts empty", f.ledger.count() === 0);
f.bus.emit("src", tb.createPacket("e", { n: 1 }));
ok("one emit -> one record", f.ledger.count() === 1);
f.bus.emit("src", tb.createPacket("e", { n: 2 }));
ok("two emits -> two records", f.ledger.count() === 2);
ok("delivered status recorded", f.ledger.all()[0].status === "delivered");
// even a REJECTED emit leaves exactly one record
var g = fixture();
var before = g.ledger.count();
threws(function () { g.bus.emit("nobody", tb.createPacket("unrouted", {})); });
ok("rejected emit still records once", g.ledger.count() === before + 1);
ok("rejection status is unrouted", g.ledger.all()[0].status === "rejected:unrouted");
})();
// ── PROPERTY 2: no undeclared path ───────────────────────────────────────────
(function () {
var f = fixture();
ok("unrouted resolve throws", threws(function () { f.routing.resolve("x", "y"); }));
ok("has() is false before register", f.routing.has("x", "y") === false);
f.routing.register({ packetType: "x", source: "y", bus: "DATA", destinations: ["d"] });
ok("has() is true after register", f.routing.has("x", "y") === true);
ok("emit of unrouted throws", threws(function () { f.bus.emit("z", tb.createPacket("x", {})); }));
// register validation
ok("register without bus throws", threws(function () { f.routing.register({ packetType: "a", source: "b", destinations: ["d"] }); }));
ok("register with empty destinations throws", threws(function () { f.routing.register({ packetType: "a", source: "b", bus: "DATA", destinations: [] }); }));
ok("register with non-array destinations throws", threws(function () { f.routing.register({ packetType: "a", source: "b", bus: "DATA", destinations: "d" }); }));
})();
// ── PROPERTY 3: a wrong-bus packet is refused ────────────────────────────────
(function () {
var routing = tb.createRoutingTable();
var ledger = tb.createLedger();
var dataBus = tb.createBus("DATA", routing, ledger);
var ctrlBus = tb.createBus("CONTROL", routing, ledger);
// route says this packet belongs on CONTROL
routing.register({ packetType: "cmd", source: "op", bus: "CONTROL", destinations: ["worker"] });
var landed = false;
dataBus.subscribe("worker", function () { landed = true; });
ok("emit on wrong bus throws", threws(function () { dataBus.emit("op", tb.createPacket("cmd", {})); }));
ok("wrong-bus packet not delivered", landed === false);
ok("wrong-bus rejection recorded", ledger.all().some(function (e) { return e.status === "rejected:wrong_bus"; }));
})();
// ── PROPERTY 4: a throwing subscriber can't break the emitter ────────────────
(function () {
var f = fixture();
f.routing.register({ packetType: "fan", source: "src", bus: "DATA", destinations: ["good", "bad", "also"] });
var goodHits = 0, alsoHits = 0;
f.bus.subscribe("good", function () { goodHits++; });
f.bus.subscribe("bad", function () { throw new Error("boom"); });
f.bus.subscribe("also", function () { alsoHits++; });
var result;
ok("emit with a throwing subscriber does not throw", !threws(function () {
result = f.bus.emit("src", tb.createPacket("fan", {}));
}));
ok("good subscriber ran", goodHits === 1);
ok("subscriber AFTER the thrower still ran", alsoHits === 1);
ok("throwing dest counted as delivered", result.delivered.indexOf("bad") !== -1);
ok("subscriber error recorded", f.ledger.all().some(function (e) { return /error:subscriber/.test(e.status); }));
// a declared destination with NO subscriber -> reported missing, not an error
var g = fixture();
g.routing.register({ packetType: "e", source: "s", bus: "DATA", destinations: ["present", "absent"] });
g.bus.subscribe("present", function () {});
var r2 = g.bus.emit("s", tb.createPacket("e", {}));
ok("present delivered", r2.delivered.indexOf("present") !== -1);
ok("absent reported missing", r2.missing.indexOf("absent") !== -1);
ok("partial status recorded", g.ledger.all().some(function (e) { return /delivered:partial/.test(e.status); }));
})();
// ── PROPERTY 5: registered routes are frozen ─────────────────────────────────
(function () {
var routing = tb.createRoutingTable();
routing.register({ packetType: "e", source: "s", bus: "DATA", destinations: ["d"] });
var entry = routing.resolve("e", "s")[0];
ok("resolved entry is frozen", Object.isFrozen(entry));
ok("entry.destinations is frozen", Object.isFrozen(entry.destinations));
// mutating a frozen entry is a silent no-op in non-strict, throws in strict — either way it must not change
var beforeBus = entry.bus;
try { entry.bus = "HACKED"; } catch (e) { /* strict-mode throw is fine */ }
ok("frozen entry.bus unchanged", entry.bus === beforeBus);
try { entry.destinations.push("injected"); } catch (e) { /* fine */ }
ok("frozen destinations unchanged", entry.destinations.length === 1);
// deepFreeze on a nested object
var nested = tb.deepFreeze({ a: { b: { c: 1 } } });
ok("deepFreeze freezes root", Object.isFrozen(nested));
ok("deepFreeze freezes nested", Object.isFrozen(nested.a.b));
})();
// ── PROPERTY 6: a trace is reconstructable ───────────────────────────────────
(function () {
var f = fixture();
f.routing.register({ packetType: "step1", source: "a", bus: "DATA", destinations: ["b"] });
f.routing.register({ packetType: "step2", source: "a", bus: "DATA", destinations: ["b"] });
f.bus.subscribe("b", function () {});
var trace = "journey-1";
f.bus.emit("a", tb.createPacket("step1", {}, { traceId: trace }));
f.bus.emit("a", tb.createPacket("step2", {}, { traceId: trace }));
f.bus.emit("a", tb.createPacket("step1", {}, { traceId: "other" }));
var hops = f.ledger.queryByTraceId(trace);
ok("trace has exactly its two hops", hops.length === 2);
ok("trace hops in emission order", hops[0].packetType === "step1" && hops[1].packetType === "step2");
ok("other trace excluded", hops.every(function (h) { return h.traceId === trace; }));
// query by packetType
ok("queryByPacketType finds all step1", f.ledger.queryByPacketType("step1").length === 2);
})();
// ── THE GENERALIZATION (tested hardest — the ported core's OWN validation) ────
// In the origin, request() was hardwired to a bus literally named 'VAULT'. The strip
// replaced that with a per-bus { requestResponse } flag. These prove the new gate.
(function () {
var routing = tb.createRoutingTable();
var ledger = tb.createLedger();
routing.register({ packetType: "q", source: "asker", bus: "RPC", destinations: ["answerer"] });
// a bus WITHOUT the flag refuses request(), regardless of its name
var plain = tb.createBus("RPC", routing, ledger); // note: named RPC, not VAULT — name must NOT matter
plain.subscribe("answerer", function () { return 99; });
ok("request refused without flag (name irrelevant)", threws(function () {
plain.request("asker", tb.createPacket("q", {}));
}));
// a bus WITH the flag answers — and the answer is the handler's return value
var routing2 = tb.createRoutingTable();
var ledger2 = tb.createLedger();
routing2.register({ packetType: "q", source: "asker", bus: "RPC", destinations: ["answerer"] });
var rpc = tb.createBus("RPC", routing2, ledger2, { requestResponse: true });
rpc.subscribe("answerer", function (pkt) { return pkt.payload.a + pkt.payload.b; });
var answer = rpc.request("asker", tb.createPacket("q", { a: 2, b: 3 }));
ok("request returns handler value", answer === 5);
ok("request_response status recorded", ledger2.all().some(function (e) { return /request_response/.test(e.status); }));
// request to an unregistered destination -> error, recorded
var routing3 = tb.createRoutingTable();
var ledger3 = tb.createLedger();
routing3.register({ packetType: "q", source: "asker", bus: "RPC", destinations: ["ghost"] });
var rpc3 = tb.createBus("RPC", routing3, ledger3, { requestResponse: true });
ok("request to unregistered dest throws", threws(function () { rpc3.request("asker", tb.createPacket("q", {})); }));
ok("unregistered-dest error recorded", ledger3.all().some(function (e) { return /destination_not_registered/.test(e.status); }));
// a throwing request handler propagates (unlike emit) but still records
var routing4 = tb.createRoutingTable();
var ledger4 = tb.createLedger();
routing4.register({ packetType: "q", source: "asker", bus: "RPC", destinations: ["answerer"] });
var rpc4 = tb.createBus("RPC", routing4, ledger4, { requestResponse: true });
rpc4.subscribe("answerer", function () { throw new Error("handler failed"); });
ok("request handler error propagates", threws(function () { rpc4.request("asker", tb.createPacket("q", {})); }));
ok("request handler error recorded", ledger4.all().some(function (e) { return /error:handler/.test(e.status); }));
})();
// ── constructor validation (the ported core's own guards) ────────────────────
(function () {
var routing = tb.createRoutingTable();
var ledger = tb.createLedger();
ok("createBus without name throws", threws(function () { tb.createBus("", routing, ledger); }));
ok("createBus without routing throws", threws(function () { tb.createBus("B", null, ledger); }));
ok("createBus without ledger throws", threws(function () { tb.createBus("B", routing, null); }));
ok("createBus with non-table routing throws", threws(function () { tb.createBus("B", {}, ledger); }));
var f = fixture();
ok("subscribe non-function throws", threws(function () { f.bus.subscribe("d", "not a fn"); }));
ok("subscribe empty dest throws", threws(function () { f.bus.subscribe("", function () {}); }));
ok("ledger.record without packet throws", threws(function () { f.ledger.record({ bus: "DATA" }); }));
// diagnostics
f.bus.subscribe("x", function () {});
ok("hasSubscriber true for x", f.bus.hasSubscriber("x") === true);
ok("hasSubscriber false for y", f.bus.hasSubscriber("y") === false);
ok("subscriberCount is 1", f.bus.subscriberCount() === 1);
})();
// ── MUTATION BITE: prove the receipt invariant is not vacuously green ─────────
// If the ledger silently dropped records, property 1's counts would still be
// "consistent" at zero. This bite asserts the ledger actually grows AND that a
// real emit produces a record whose fields tie back to the packet — so a no-op
// record() (the plausible mutation) fails loud here.
(function () {
var f = fixture();
f.routing.register({ packetType: "bite", source: "s", bus: "DATA", destinations: ["d"] });
f.bus.subscribe("d", function () {});
var pkt = tb.createPacket("bite", { proof: true });
f.bus.emit("s", pkt);
var rec = f.ledger.all()[0];
ok("mutation bite: ledger actually grew", f.ledger.count() === 1);
ok("mutation bite: record ties to the packet", rec && rec.packetId === pkt.packetId && rec.traceId === pkt.traceId);
ok("mutation bite: record carries real status", rec && typeof rec.status === "string" && rec.status.length > 0);
})();
console.log((fail === 0 ? "PASS" : "FAIL") + " — " + pass + " passed, " + fail + " failed");
process.exit(fail === 0 ? 0 : 1);