From 08938b9ee3345bc90adce933002e4dc8e2380c4c Mon Sep 17 00:00:00 2001 From: nzneit Date: Sat, 1 Aug 2026 15:52:06 -0400 Subject: [PATCH] chore(deps): drop aedes-server-factory; the tcp listener is bare node:net Remove the aedes-server-factory dependency (^0.2.1) and replace its one call site with net.createServer((socket) => aedes.handle(socket)) in src/broker/index.ts. Recorded as D-024. The package has been dead since 2021-06-28, hard-depends on ws@7 (the exact package whose Bun stand-in forced the hand-built ws listener in the first place) and aedes-protocol-decoder, and offbook used it for a single line. Read from its source: with no options its path reduces to net.createServer + aedes.handle plus a req.connDetails field, and nothing in the repo reads connDetails (repo-wide grep, exit 1), so the replacement is behaviorally equivalent for offbook's use. The removal drops aedes-server-factory, aedes-protocol-decoder and ws@7.5.11 from the lockfile; ws@8 remains via mqtt (dev) and the Stryker runner. This is the one aedes-adjacent change that does NOT move the floor under R-006/R-007: the spike surface (aedes core at 0.51.x + the hand-built ws listener) is untouched. D-021's deferral of aedes 1.x stands, narrowed - the 1.x migration no longer involves replacing this package. D-024 also records the probe results that de-risk that future bump: aedes 1.1.1 runs under Bun (10-check probe, exit 0, driven exactly the way src/broker drives 0.51), with a migration delta of roughly ten lines. The Biome noRestrictedImports entry and the transport-isolation regex both keep listing the package deliberately: they now guard against reintroduction. Negative control verified with the package uninstalled: a planted aedes-server-factory import outside src/broker/ makes lint exit 1; the clean tree exits 0. New test in src/broker/fingerprint.test.ts: the tcp listener round-trips data both ways (a QoS-1 publish reaches onInbound, an emit reaches a tcp subscriber). The pre-existing tcp coverage stopped at the CONNECT handshake, which would not have caught a listener that handshakes but drops data. Gates: check-docs, lint, typecheck, demo-app:build and full bun test all exit 0. --- DECISIONS.md | 14 ++++++++++++++ bun.lock | 11 ----------- package.json | 1 - src/broker/fingerprint.test.ts | 28 ++++++++++++++++++++++++++++ src/broker/index.ts | 10 ++++++++-- 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/DECISIONS.md b/DECISIONS.md index 31ec6f4..3cf9b2c 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -221,3 +221,17 @@ Append-only. Each decision has a stable never-reused `D-###` id, what was decide **Obligations**: none blocking. One optional follow-up: remove the unused `_config` parameter from `createServer` in a change scoped to that refactor. **From**: D-021's dependency review, which measured the bump as viable, recorded the `biome migrate` landmine, and deferred the work to its own PR. **Folds into**: package.json, bun.lock, biome.json, test/lint-gate.test.ts, src/registry/index.ts, src/engine/index.ts, src/control-plane/index.ts, AGENTS.md (working notes), plus import ordering across 17 files and the 14 optional-chain sites + +### D-024: Drop `aedes-server-factory`; the tcp listener is bare node:net +**Date**: 2026-08-01 +**What**: Remove the `aedes-server-factory` dependency (`^0.2.1`) and replace its one call site with `net.createServer((socket) => aedes.handle(socket))` in `src/broker/index.ts`. The ws listener was already hand-built (that package's ws path never worked under Bun; see the note in `src/broker/index.ts`), so after this `broker/` drives aedes with no intermediary on either transport. +**Why**: The package has been dead since 2021-06-28, hard-depends on `ws@7` (the exact package whose Bun stand-in forced the hand-built ws listener in the first place) and `aedes-protocol-decoder`, and offbook used it for a single line. D-021 already identified it as "not a maintained path to 1.x"; removing it now is the one aedes-adjacent change that does **not** move the floor under R-006/R-007, because the spike surface (aedes core at 0.51.x + the hand-built ws listener) is untouched. +**Measured (2026-08-01, published tarballs + live probes)**: +- **The replacement is behaviorally equivalent for offbook's use.** Read from `aedes-server-factory@0.2.1`'s source: with no options its path is `net.createServer((conn) => bindConnection(...))`, and `bindConnection` without `trustProxy` is `req.connDetails = extractSocketDetails(conn); aedes.handle(conn, req)`. Nothing in the repo reads `connDetails` (repo-wide grep, exit 1), so the only delta is an unread metadata field. +- Dropping it removes `aedes-server-factory`, `aedes-protocol-decoder` and `ws@7.5.11` from the lockfile. `ws@8` remains via `mqtt` (dev dependency) and the Stryker runner; neither is a runtime path of `broker/`. +- **Recorded here for D-021 obligation (1): aedes 1.1.1 runs under Bun.** A 10-check probe driving it exactly the way `src/broker/index.ts` drives 0.51 — named-class construction plus `await listen()`, `preConnect`, `handle()` on a raw socket, the `on("publish")`/`on("subscribe")` shapes, `publish(packet, cb)`, `createRetainedStream("#")` consumed as a Readable (aedes-persistence 10 wraps its async generator in `Readable.from`), `close(cb)` — exits 0. The migration delta is three changes of roughly ten lines total: the named import (the 1.x default export throws a migration error), `await aedes.listen()` inside `start()` (persistence setup became async and moved out of the constructor), and this entry's node:net listener. The deferral itself **stands**: 1.x changes runtime defaults (a new `drainTimeout: 60000`, a `maxTopicLevels` clamp, keepalive limits) and R-006/R-007 measure ws fidelity against the broker's defaults, so the spikes still run first, on 0.51.x, and the bump lands after them with an R-033 rehearsal re-run. +**Mitigations / notes**: The Biome `noRestrictedImports` entry for `aedes-server-factory` and the `test/transport-isolation.test.ts` regex both keep listing the package deliberately — they now guard against reintroduction, and `test/lint-gate.test.ts` continues to assert the Biome entry unchanged. A new test in `src/broker/fingerprint.test.ts` proves the replacement listener carries data both ways over tcp (a QoS-1 publish reaches `onInbound`, an `emit` reaches a tcp subscriber); the pre-existing tcp coverage stopped at the CONNECT handshake. Historical documents (`docs/plans/`, D-021's own text) keep naming the package and are not rewritten. +**Consequences for earlier entries**: narrows D-021 obligation (1) — the aedes 1.x migration no longer involves replacing this package, and the "aedes-server-factory is not a maintained path to 1.x" clause is discharged by removal. The 1.x bump itself remains deferred behind R-006/R-007. +**Obligations**: none. +**From**: the "matters of aedes" follow-up to D-021 (2026-08-01): source reads of `aedes-server-factory@0.2.1`, tarball reads of `aedes@1.1.1` and `aedes-persistence@10.3.1`, and the Bun probe described above. +**Folds into**: package.json, bun.lock, src/broker/index.ts, src/broker/fingerprint.test.ts diff --git a/bun.lock b/bun.lock index 19e3a58..d38ff7e 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,6 @@ "dependencies": { "@asyncapi/parser": "^3.6.0", "aedes": "^0.51.3", - "aedes-server-factory": "^0.2.1", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "hono": "^4.6.0", @@ -279,10 +278,6 @@ "aedes-persistence": ["aedes-persistence@9.1.2", "", { "dependencies": { "aedes-packet": "^3.0.0", "qlobber": "^7.0.0" } }, "sha512-2Wlr5pwIK0eQOkiTwb8ZF6C20s8UPUlnsJ4kXYePZ3JlQl0NbBA176mzM8wY294BJ5wybpNc9P5XEQxqadRNcQ=="], - "aedes-protocol-decoder": ["aedes-protocol-decoder@2.2.0", "", { "dependencies": { "forwarded": "^0.2.0", "proxy-protocol-js": "^4.0.6" } }, "sha512-VeYR3Gx8KQ+VP/yX1hArwxoBeZJ3wPEg0LoLWfv6qfL/7RkClcxfOumV3nW2gCYs1a/0EMxpooZjn2nhfyFlxA=="], - - "aedes-server-factory": ["aedes-server-factory@0.2.1", "", { "dependencies": { "aedes-protocol-decoder": "^2.0.1", "ws": "^7.5.0" } }, "sha512-pVN5pKyi1UDZRU5DFbL/OnrMmd9xmOfAJ+NqG60mUrTt5nL2ERMJ9j/Of5MrKINz/YH7vctJ8UIhxlVj4zJyKQ=="], - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], @@ -433,8 +428,6 @@ "for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="], - "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], "function.prototype.name": ["function.prototype.name@1.2.0", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2", "hasown": "^2.0.4", "is-callable": "^1.2.7", "is-document.all": "^1.0.0" } }, "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew=="], @@ -649,8 +642,6 @@ "progress": ["progress@2.0.3", "", {}, "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA=="], - "proxy-protocol-js": ["proxy-protocol-js@4.0.6", "", {}, "sha512-SjXgyBmr0dBbKUZ0jOzp0N9urTcDOI1cd1oEeE43W1vG4OMwYYLggCRcMJ0zv0gdTA8Imb4cAiYj8Ic/PWv1mw=="], - "qlobber": ["qlobber@7.0.1", "", {}, "sha512-FsFg9lMuMEFNKmTO9nV7tlyPhx8BmskPPjH2akWycuYVTtWaVwhW5yCHLJQ6Q+3mvw5cFX2vMfW2l9z2SiYAbg=="], "qs": ["qs@6.15.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg=="], @@ -849,8 +840,6 @@ "aedes-packet/mqtt-packet": ["mqtt-packet@7.1.2", "", { "dependencies": { "bl": "^4.0.2", "debug": "^4.1.1", "process-nextick-args": "^2.0.1" } }, "sha512-FFZbcZ2omsf4c5TxEQfcX9hI+JzDpDKPT46OmeIBpVA7+t32ey25UNqlqNXTmeZOr5BLsSIERpQQLsFWJS94SQ=="], - "aedes-server-factory/ws": ["ws@7.5.11", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-zS54Oen9bITtp7kp2XM3AydrCIq1D+HwJOuH+c+e4LfpL/lotP5osijd+UoMnxwAam1GN8R4KtLAyIrIcBNpiA=="], - "concat-stream/readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], "cross-spawn/path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], diff --git a/package.json b/package.json index 33dcbde..2ea2213 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,6 @@ "dependencies": { "@asyncapi/parser": "^3.6.0", "aedes": "^0.51.3", - "aedes-server-factory": "^0.2.1", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "hono": "^4.6.0", diff --git a/src/broker/fingerprint.test.ts b/src/broker/fingerprint.test.ts index 072a5f5..f032fb2 100644 --- a/src/broker/fingerprint.test.ts +++ b/src/broker/fingerprint.test.ts @@ -4,6 +4,7 @@ import { afterAll, beforeAll, expect, test } from "bun:test"; import { MqttClient, connectAsync as mqttConnectAsync } from "mqtt"; import tcpStreamBuilder from "mqtt/lib/connect/tcp"; import { loadConfig } from "#src/config/index.ts"; +import type { InboundEvent } from "#src/model/index.ts"; import type { BrokerModule, FingerprintEvent } from "./index.ts"; import { createBroker, fingerprintLine } from "./index.ts"; @@ -118,6 +119,33 @@ test("a tcp CONNECT emits a fingerprint with no ws block", async () => { await client.endAsync(); }); +// The tcp listener is bare node:net (D-024, replacing `aedes-server-factory`); +// the CONNECT test above only proves the handshake, so this one proves data +// flows through it in both directions. +test("the tcp listener round-trips data: a tcp publish reaches onInbound, an emit reaches a tcp subscriber", async () => { + const inbound: InboundEvent[] = []; + broker.onInbound((e) => inbound.push(e)); + const client = await connectAsync(`mqtt://localhost:${TCP}`, { + reconnectPeriod: 0, + clientId: "fp-tcp-rt", + }); + const got = new Promise((resolve) => + client.on("message", (topic, payload) => + resolve(`${topic} ${payload.toString()}`), + ), + ); + await client.subscribeAsync("state/tcp-rt", { qos: 1 }); + await client.publishAsync("command/tcp-rt", JSON.stringify({ n: 7 }), { + qos: 1, + }); + await broker.emit({ topic: "state/tcp-rt", payload: { ok: true }, qos: 1 }); + expect(await got).toBe('state/tcp-rt {"ok":true}'); + const seen = inbound.find((e) => e.message.topic === "command/tcp-rt"); + expect(seen?.message.payload).toEqual({ n: 7 }); + expect(seen?.message.qos).toBe(1); + await client.endAsync(); +}); + test("subscribe observations dedupe per clientId·topic·qos; publish per clientId·qos·retain", async () => { const client = await connectAsync(`ws://localhost:${WS}`, { forceNativeWebSocket: true, diff --git a/src/broker/index.ts b/src/broker/index.ts index e2e1ed8..eed576a 100644 --- a/src/broker/index.ts +++ b/src/broker/index.ts @@ -1,6 +1,6 @@ +import net from "node:net"; import { Duplex } from "node:stream"; import Aedes from "aedes"; -import { createServer } from "aedes-server-factory"; import type { ServerWebSocket } from "bun"; import type { Config, @@ -280,7 +280,13 @@ export function createBroker(config: Config): BrokerModule { }, }) as AedesWithPersistence; const wsServer = createWsListener(aedes, wsFacts); - const tcpServer = createServer(aedes); + // Bare node:net on purpose — `aedes-server-factory`'s no-options path + // reduced to exactly this plus a `req.connDetails` nothing here reads, and + // the package is unmaintained with a hard ws@7 dependency (the same package + // whose Bun stand-in is why the ws listener above is hand-built). D-024. + const tcpServer = net.createServer((socket) => { + aedes.handle(socket); + }); let seq = 0; const inbound: Array<(e: InboundEvent) => void> = []; const subs: Array<(s: { topic: string; clientId: string }) => void> = [];