A configurable Node.js + TypeScript framework for mapping data between protocols. It ingests from one or more Ingress adapters, normalises updates into a protocol-neutral internal model, and re-emits them through one or more Egress adapters.
Phase 1 ships MQTT and NMOS IS-12 / MS-05 in both ingress and egress roles (MQTT↔MQTT relay, IS-12 client↔local device, MQTT↔IS-12 cross-protocol, and combinations with fan-out). Adapter choice is configuration only — the core knows nothing about any specific protocol.
The system is built around three roles:
- Ingress — the source-facing adapter that brings external data into the engine (e.g. an MQTT subscriber). It translates raw wire payloads into normalised operations.
- UCE (Unified Communication Engine) — the protocol-neutral core and single source of truth. It holds an object tree (objects with typed properties, methods and nested children), a flexible type system, a serialization layer, and an internal message bus. It depends on no protocol.
- Egress — the consumer-facing adapter that projects the engine outward (e.g. an NMOS IS-12 WebSocket server). Multiple Egress endpoints can run at once (fan-out).
Ingress source ──► Ingress Adapter ──► UCE (object tree + types + bus) ──► Egress Adapter ──► consumers
(e.g. MQTT) (e.g. MQTT) ◄── single source of truth ◄── (e.g. IS-12)
write-back ◄───────────────────────────────────────────────────────────────────┘
Synchronization is bidirectional by design. A change to a property — from either side — is
propagated to every adapter that maps it, with origin tagging and echo suppression to prevent feedback
loops. An IS-12 Set, for example, updates the UCE and is written back to the mapped MQTT topic while
also notifying other IS-12 controllers.
The engine never depends on an adapter or any protocol package. This one-way dependency
(adapters → engine, never the reverse) is enforced automatically by dependency-cruiser
(npm run arch:check), so new protocols can be added as pure adapters with no engine changes.
- Node.js 22+ LTS
- npm (ships with Node)
npm install
npm run build# Run the built application (requires a bridge config path)
npm start -- Scenarios/Scenario-01/bridge.yaml
# Print build version / provenance (requires a prior `npm run build`)
node dist/app.js --version
# Or run in watch mode during development (no build step needed)
npm run dev -- Scenarios/Scenario-01/bridge.yamlThe entry point is src/app.ts. Worked examples live under Scenarios/:
| Scenario | Description |
|---|---|
| Scenario-01 | Minimal MQTT → IS-12 string mapping |
| Scenario-02 | MQTT numeric → NcReceiverMonitor linkStatus |
| Scenario-03 | Dual monitors, per-domain-status MQTT, derived overallStatus |
| Scenario-04 | IS-12 ingress (client) → MQTT egress; bidirectional userLabel |
| Scenario-05 | MQTT ↔ MQTT bidirectional topic relay (independent source/dest broker connections) |
| Scenario-06 | IS-12 ingress (client) → IS-12 egress; remote receivers ↔ local egress block userLabel sync |
Each scenario includes its own bridge.yaml, model, mappings, and runbook.
The project uses Vitest with v8 coverage.
npm test # run once
npm run test:watch # re-run on change
npm run coverage # tests + coverage thresholdsA single command mirrors the CI pipeline and is the recommended local check before committing:
npm run validate # typecheck → lint → arch:check → coverage
npm run audit:check # fail on high+ severity npm advisories
npm run sbom # CycloneDX SBOM → sbom.jsonIndividual gates:
| Command | Purpose |
|---|---|
npm run build |
Compile to dist/ and write dist/build-info.json (version stamp) |
npm run typecheck |
TypeScript strict type-checking (no emit) |
npm run lint |
ESLint (typescript-eslint, import hygiene, no-floating-promises) |
npm run format:check |
Prettier formatting check (npm run format to fix) |
npm run arch:check |
Enforces the engine → adapter independence boundary |
npm run coverage |
Tests + coverage thresholds |
npm run audit:check |
Dependency audit — fails on high+ severity CVEs |
npm run sbom |
Generate CycloneDX SBOM for supply-chain visibility |
A husky pre-commit hook runs lint-staged to lint and format staged files automatically.
Push a semver tag (v*) to trigger the release workflow: it builds a stamped artifact, generates an SBOM,
writes a changelog from commits since the previous tag, and publishes a GitHub Release with the tarball
and sbom.json attached.
src/
engine/ # Protocol-neutral core (UCE) — no protocol imports allowed
types/ # Datatype, DatatypeRegistry, constraints, primitives
model/ # ObjectNode, ObjectTree, descriptors
bus/ # UceBus + Operation definitions
serialization/ # toJSON / snapshot / marshalling
propertyRelay.ts # Optional UCE property mirroring (bridge.yaml relays)
adapters/ # The only protocol-aware components
mqtt/ # Ingress + egress: MQTT
nmos-is12/ # Ingress (client) + egress (device): IS-12 / MS-05
mapping/ # Config-driven Ingress/Egress translation DSLs + transforms
config/ # YAML config loading + schema validation
observability/ # Structured logging, metrics, health
app.ts # Bootstrap entry point
Scenarios/ # Worked bridge examples (model, mappings, bridge.yaml, README)
test/ # Unit + integration tests (mirrors src/ layout)
| Concern | Choice |
|---|---|
| Runtime / language | Node.js 22+ LTS, TypeScript 6.x (strict, ESM) |
MQTT (mqtt) |
Ingress and egress — subscribe, publish, write-back |
IS-12 / MS-05 (ws) |
Ingress client (NCP controller) and egress device (NCP server) |
| IS-04 Node API | Optional on IS-12 egress — Node.js http + shared WebSocket port |
| Schema validation | zod (bridge, model, adapter config at runtime) |
| Wire-schema tests | ajv (IS-12 JSON Schema compliance in test suite) |
| Config | yaml |
| Dev runner | tsx (npm run dev) |
| Logging | pino |
| Testing | vitest (unit, integration, e2e); fast-check (property/fuzz) |
| Supply chain | @cyclonedx/cyclonedx-npm (npm run sbom) |
| Lint / format | eslint + prettier; husky + lint-staged (pre-commit) |
| Architecture enforcement | dependency-cruiser (npm run arch:check) |