diff --git a/package-lock.json b/package-lock.json index 94b0e43..3cbcc0d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,8 @@ "strip-json-comments": "5.0.3", "tcp-port-used": "1.0.2", "tslib": "2.6.2", - "typescript": "5.9.3" + "typescript": "5.9.3", + "undici": "8.4.1" }, "bin": { "bitsocial": "bin/run" @@ -3951,15 +3952,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/@libp2p/http/node_modules/undici": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-8.2.0.tgz", - "integrity": "sha512-Z+4Hx9GE26Lh9Upwfnc8C7SsrpBPGaM/Gm6kMFtiG7c+5IvQKlXi/t+9x9DrrCh29cww5TSP9YdVaBcnLDs5fQ==", - "license": "MIT", - "engines": { - "node": ">=22.19.0" - } - }, "node_modules/@libp2p/identify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/@libp2p/identify/-/identify-4.1.3.tgz", @@ -9759,6 +9751,15 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/cheerio/node_modules/undici": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", + "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -21181,6 +21182,15 @@ "node": ">=20.0.0" } }, + "node_modules/open-graph-scraper/node_modules/undici": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.27.2.tgz", + "integrity": "sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -25764,12 +25774,12 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.4.1.tgz", + "integrity": "sha512-RNHlB4fxZK0IrkhBsxhlbx7s8kFWwr7rzzOqj5nvZugw3ig3RsB7KW3zVlV0eu8POl+rx5d1hmL7rRg0z1owow==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=22.19.0" } }, "node_modules/undici-types": { diff --git a/package.json b/package.json index b92bb6f..1cf1bb8 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,8 @@ "strip-json-comments": "5.0.3", "tcp-port-used": "1.0.2", "tslib": "2.6.2", - "typescript": "5.9.3" + "typescript": "5.9.3", + "undici": "8.4.1" }, "overrides": {}, "webuis": [ diff --git a/test/cli/undici-dispatcher-hoisting.test.ts b/test/cli/undici-dispatcher-hoisting.test.ts new file mode 100644 index 0000000..8356eb1 --- /dev/null +++ b/test/cli/undici-dispatcher-hoisting.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { createRequire } from "module"; +import { readFileSync } from "fs"; +import { join } from "path"; + +/** + * Regression guard for issue #84. + * + * pkc-js disables undici's default 5-minute `bodyTimeout` (so idle pubsub long-poll + * streams are not aborted) via `setGlobalDispatcher(new Agent({ bodyTimeout: MAX }))`, + * importing the top-level `undici`. + * + * undici changed its global-dispatcher symbol between v7 and v8: + * - undici 7.x: Symbol.for("undici.globalDispatcher.1") + * - undici 8.x: Symbol.for("undici.globalDispatcher.2") (.1 is legacy only) + * + * `setGlobalDispatcher` writes the slot of whichever undici is hoisted to top-level. + * The idle pubsub stream is dispatched on the undici copy that `@libp2p/http` (pulled by + * helia) uses, which requires `undici@^8` and therefore reads the `.2` slot. If a `^7` + * undici wins the top-level hoist, pkc-js writes `.1`, the `@libp2p/http` request reads + * `.2` (never set), and the stream times out after 5 minutes (UND_ERR_BODY_TIMEOUT) — + * reproducible in Docker but not on a host install where undici 8.x hoists to top. + * + * Invariant: the undici pkc-js patches (top-level) and the undici `@libp2p/http` uses must + * be the same major (>= 8), so both read/write the same global-dispatcher symbol and the + * polyfill is effective. + */ +describe("undici global-dispatcher hoisting (issue #84)", () => { + const projectRoot = process.cwd(); + const majorOf = (pkgJsonPath: string): number => { + const version: string = JSON.parse(readFileSync(pkgJsonPath, "utf-8")).version; + return Number(version.split(".")[0]); + }; + + it("hoists undici 8.x to top-level so pkc-js's body-timeout polyfill writes the .2 slot", () => { + const fromRoot = createRequire(join(projectRoot, "package.json")); + const topUndiciPkg = fromRoot.resolve("undici/package.json"); + expect(majorOf(topUndiciPkg)).toBeGreaterThanOrEqual(8); + }); + + it("resolves @libp2p/http's undici to the same major as the top-level copy", () => { + const fromRoot = createRequire(join(projectRoot, "package.json")); + // Anchor on @libp2p/http's main entry (its exports map blocks ./package.json), + // then resolve undici from there: a nested copy if hoisting regressed, else top-level. + const httpMain = fromRoot.resolve("@libp2p/http"); + const httpUndiciPkg = createRequire(httpMain).resolve("undici/package.json"); + const topUndiciPkg = fromRoot.resolve("undici/package.json"); + + const httpMajor = majorOf(httpUndiciPkg); + const topMajor = majorOf(topUndiciPkg); + + expect(httpMajor).toBeGreaterThanOrEqual(8); + // Same major => same global-dispatcher symbol => pkc-js polyfill governs this path. + expect(httpMajor).toBe(topMajor); + }); +});