diff --git a/.gitignore b/.gitignore index 6aabf12..0e2529b 100644 --- a/.gitignore +++ b/.gitignore @@ -119,3 +119,4 @@ dist .pnp.* tmp/ +test/download diff --git a/README.md b/README.md index c25eba1..29c7654 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Thanks to undici: - Support [HTTP redirections](https://developer.mozilla.org/en-US/docs/Web/HTTP/Redirections) with the `maxRedirections` argument. - Implement high-level API for undici **stream** and **pipeline** method. -- Optimization and performance of the new client (**around 2.5x faster** than Node.js native http client). +- High performance (see [benchmarks](https://undici.nodejs.org/#/?id=benchmarks)). - Work well with **newest** Node.js API [AbortController](https://nodejs.org/dist/latest-v16.x/docs/api/globals.html#globals_class_abortcontroller) to cancel http request. Light with seriously maintained dependencies: diff --git a/src/stream.ts b/src/stream.ts index 2928280..ccf98d7 100644 --- a/src/stream.ts +++ b/src/stream.ts @@ -9,7 +9,9 @@ import { ReqOptions } from "./request"; import { computeURI } from "./agents"; import * as Utils from "./utils"; -export function pipeline(method: string, uri: string | URL, options: Omit = {}): Duplex { +export type StreamOptions = Omit; + +export function pipeline(method: string, uri: string | URL, options: StreamOptions = {}): Duplex { const { maxRedirections = 0 } = options; const computedURI = computeURI(uri); @@ -24,7 +26,7 @@ export function pipeline(method: string, uri: string | URL, options: Omit Promise; -export function stream(method: string, uri: string | URL, options: Omit = {}): WritableStreamCallback { +export function stream(method: string, uri: string | URL, options: StreamOptions = {}): WritableStreamCallback { const { maxRedirections = 0 } = options; const computedURI = computeURI(uri); diff --git a/test/agents.spec.ts b/test/agents.spec.ts index b33b855..8271580 100644 --- a/test/agents.spec.ts +++ b/test/agents.spec.ts @@ -91,4 +91,13 @@ describe("computeURI", () => { expect(result).toStrictEqual(true); }); + + it("should compute an URL not related to any local agents", () => { + const stringURL = "https://www.linkedin.com/feed/"; + const result = Agents.computeURI(new URL("", stringURL)); + + expect(result.url.href).toStrictEqual(stringURL); + expect(result.agent).toStrictEqual(null); + expect(result.limit).toStrictEqual(undefined); + }); }); diff --git a/test/fixtures/.gitkeep b/test/fixtures/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/test/fixtures/home.html b/test/fixtures/home.html new file mode 100644 index 0000000..1bda92a --- /dev/null +++ b/test/fixtures/home.html @@ -0,0 +1,12 @@ + + + + + + + Document + + +

hello world

+ + diff --git a/test/fixtures/lorem.txt b/test/fixtures/lorem.txt new file mode 100644 index 0000000..1b37687 --- /dev/null +++ b/test/fixtures/lorem.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. diff --git a/test/jest.setup.js b/test/jest.setup.js index cb96a9f..5573471 100644 --- a/test/jest.setup.js +++ b/test/jest.setup.js @@ -1 +1 @@ -jest.setTimeout(20000); +jest.setTimeout(25000); diff --git a/test/request.spec.ts b/test/request.spec.ts index bbbfbb1..2e4156c 100644 --- a/test/request.spec.ts +++ b/test/request.spec.ts @@ -25,14 +25,14 @@ describe("http.get", () => { expect(typeof data.uptime).toStrictEqual("number"); }); - it("should GET a ressource with an http redirection", async() => { + it("should GET uptime by following an HTTP redirection from local fastify server", async() => { const { data } = await get<{ uptime: number }>("/local/redirect", { maxRedirections: 1 }); expect("uptime" in data).toStrictEqual(true); expect(typeof data.uptime).toStrictEqual("number"); }); - it("should GET uptime with a limit wrapper", async() => { + it("should GET uptime through a limit function handler from local fastify server", async() => { let executed = false; // eslint-disable-next-line func-style const limit = (callback) => { @@ -81,7 +81,7 @@ describe("http.get", () => { } }); - it("should throw a SyntaxError with jsonError endpoint", async() => { + it("should throw a 'SyntaxError' with jsonError endpoint from local fastify server", async() => { expect.assertions(1); try { diff --git a/test/server/index.ts b/test/server/index.ts index 3becd0a..0228e93 100644 --- a/test/server/index.ts +++ b/test/server/index.ts @@ -1,3 +1,8 @@ +// Import Node.js Dependencies +import path from "path"; +import fs from "fs"; +import { Transform } from "stream"; + // Import Third-party Dependencies import fastify from "fastify"; import * as undici from "undici"; @@ -5,6 +10,21 @@ import * as undici from "undici"; // Import Internal Dependencies import { CustomHttpAgent, agents } from "../../src/agents"; +// CONSTANTS +const kFixturesPath = path.join(__dirname, "..", "fixtures"); + +const toUpperCase = new Transform({ + transform(chunk, enc, next) { + for (let id = 0; id < chunk.length; id++) { + const char = chunk[id]; + chunk[id] = char < 97 || char > 122 ? char : char - 32; + } + + this.push(chunk); + next(); + } +}); + export async function createServer(customPath = "local", port = 3000) { const server = fastify({ logger: false }); const serverAgent: CustomHttpAgent = { @@ -27,6 +47,18 @@ export async function createServer(customPath = "local", port = 3000) { }; }); + server.get("/home", (request, reply) => { + reply.send( + fs.createReadStream(path.join(kFixturesPath, "home.html")) + ); + }); + + server.get("/pipeline", (request, reply) => { + reply.send( + request.raw.pipe(toUpperCase) + ); + }); + server.get("/redirect", (request, reply) => { reply.redirect("/"); }); diff --git a/test/stream.spec.ts b/test/stream.spec.ts new file mode 100644 index 0000000..cc87fc9 --- /dev/null +++ b/test/stream.spec.ts @@ -0,0 +1,79 @@ +// Import Third-party Dependencies +import { FastifyInstance } from "fastify"; + +// Import Node.js Dependencies +import { createWriteStream, createReadStream, existsSync, promises as fs } from "fs"; +import path from "path"; +import { pipeline } from "stream/promises"; + +// Import Internal Dependencies +import * as httpie from "../src/index"; +import { createServer } from "./server/index"; + +// CONSTANTS +const kGithubURL = new URL("https://github.com/"); +const kFixturesPath = path.join(__dirname, "fixtures"); +const kDownloadPath = path.join(__dirname, "download"); + +let httpServer: FastifyInstance; +beforeAll(async() => { + httpServer = await createServer("stream", 1338); + await fs.mkdir(kDownloadPath, { recursive: true }); +}); + +afterAll(async() => { + await httpServer.close(); + await fs.rm(kDownloadPath, { force: true, recursive: true }); +}); + +describe("stream", () => { + it("should fetch a .tar.gz of a given github repository", async() => { + const fileDestination = path.join(kDownloadPath, "i18n-main.tar.gz"); + const repositoryURL = new URL("NodeSecure/i18n/archive/main.tar.gz", kGithubURL); + + await httpie.stream("GET", repositoryURL, { + headers: { + "User-Agent": "httpie", + "Accept-Encoding": "gzip, deflate" + }, + maxRedirections: 1 + })(createWriteStream(fileDestination)); + + expect(existsSync(fileDestination)).toStrictEqual(true); + }); + + it("should fetch the HTML home from the local fastify server", async() => { + const fileDestination = path.join(kDownloadPath, "home.html"); + + await httpie.stream("GET", "/stream/home")(createWriteStream(fileDestination)); + + expect(existsSync(fileDestination)).toStrictEqual(true); + const [contentA, contentB] = await Promise.all([ + fs.readFile(path.join(kFixturesPath, "home.html"), "utf-8"), + fs.readFile(path.join(kDownloadPath, "home.html"), "utf-8") + ]); + + expect(contentA).toStrictEqual(contentB); + }); +}); + +describe("pipeline", () => { + it("should be able to pipeline (duplex stream)", async() => { + const fixtureLocation = path.join(kFixturesPath, "lorem.txt"); + const fileDestination = path.join(kDownloadPath, "lorem.txt"); + + await pipeline( + createReadStream(fixtureLocation), + httpie.pipeline("GET", "/stream/pipeline"), + createWriteStream(fileDestination) + ); + + expect(existsSync(fileDestination)).toStrictEqual(true); + const [contentA, contentB] = await Promise.all([ + fs.readFile(fixtureLocation, "utf-8"), + fs.readFile(fileDestination, "utf-8") + ]); + + expect(contentA.toUpperCase()).toStrictEqual(contentB); + }); +});