Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ jobs:
./packages/node-cache/coverage/lcov.info, \
./packages/memoize/coverage/lcov.info, \
./packages/memory/coverage/lcov.info, \
./packages/net/coverage/lcov.info, \
./packages/utils/coverage/lcov.info
19 changes: 19 additions & 0 deletions packages/net/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
MIT License & © Jared Wray

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
43 changes: 43 additions & 0 deletions packages/net/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[<img align="center" src="https://cacheable.org/logo.svg" alt="Cacheable" />](https://github.com/jaredwray/cacheable)

> High Performance Network Caching for Node.js with fetch, request, http 1.1, and http 2 support

[![codecov](https://codecov.io/gh/jaredwray/cacheable/graph/badge.svg?token=lWZ9OBQ7GM)](https://codecov.io/gh/jaredwray/cacheable)
[![tests](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml/badge.svg)](https://github.com/jaredwray/cacheable/actions/workflows/tests.yml)
[![npm](https://img.shields.io/npm/dm/@cacheable/net.svg)](https://www.npmjs.com/package/@cacheable/net)
[![npm](https://img.shields.io/npm/v/@cacheable/net.svg)](https://www.npmjs.com/package/@cacheable/net)
[![license](https://img.shields.io/github/license/jaredwray/cacheable)](https://github.com/jaredwray/cacheable/blob/main/LICENSE)


Features:
* `fetch` from [undici](https://github.com/nodejs/undici) cache enabled via `cacheable`
* `fetch` quick helpers such as `get`, `post`, `put`, and `delete` for easier development
* `request` from [undici](https://github.com/nodejs/undici) cache enabled via `cacheable`
* HTTP/1.1 and HTTP/2 caching support via Node.js `http` and `https` modules
* [RFC 7234](http://httpwg.org/specs/rfc7234.html) compliant HTTP caching for native Node.js HTTP/HTTPS requests
* Drop in replacement for `http` `https`, `fetch` modules with caching enabled
* DNS caching for `dns.lookup` and `dns.resolve` methods via `cacheable`
* WHOIS caching for `whois.lookup` method via `cacheable`
* Advanced key generation via built in hashing and custom key generation functions
* Benchmarks for performance comparison
* All the features of [cacheable](https://npmjs.com/package/cacheable) - layered caching, LRU, expiration, hooks, backed by Keyv, and more!
* Highly Tested and Maintained on a regular basis with a focus on performance and reliability

# Table of Contents
* [Getting Started](#getting-started)
* [How to Contribute](#how-to-contribute)
* [License and Copyright](#license-and-copyright)

# Getting Started

```bash
npm install @cacheable/net
```


# How to Contribute

You can contribute by forking the repo and submitting a pull request. Please make sure to add tests and update the documentation. To learn more about how to contribute go to our main README [https://github.com/jaredwray/cacheable](https://github.com/jaredwray/cacheable). This will talk about how to `Open a Pull Request`, `Ask a Question`, or `Post an Issue`.

# License and Copyright
[MIT © Jared Wray](./LICENSE)
77 changes: 77 additions & 0 deletions packages/net/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
{
"name": "@cacheable/net",
"version": "1.0.0",
"description": "High Performance Network Caching for Node.js with fetch, request, http 1.1, and http 2 support",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/index.cjs",
"import": "./dist/index.js"
}
},
"repository": {
"type": "git",
"url": "git+https://github.com/jaredwray/cacheable.git",
"directory": "packages/cacheable"
},
"author": "Jared Wray <me@jaredwray.com>",
"license": "MIT",
"private": false,
"scripts": {
"build": "rimraf ./dist && tsup src/index.ts --format cjs,esm --dts --clean",
"prepublish": "pnpm build",
"test": "xo --fix && vitest run --coverage",
"test:ci": "xo && vitest run --coverage",
"clean": "rimraf ./dist ./coverage ./node_modules"
},
"devDependencies": {
"@faker-js/faker": "^9.9.0",
"@types/eslint": "^9.6.1",
"@types/node": "^24.1.0",
"@vitest/coverage-v8": "^3.2.4",
"rimraf": "^6.0.1",
"tsup": "^8.5.0",
"typescript": "^5.8.3",
"vitest": "^3.2.4",
"xo": "^1.2.1"
},
"dependencies": {
"cacheable": "workspace:^",
"hookified": "^1.10.0",
"undici": "^7.13.0"
},
"keywords": [
"cacheable",
"http caching",
"fetch caching",
"request caching",
"http 1.1 caching",
"http 2 caching",
"dns caching",
"whois caching",
"high performance",
"layer 1 caching",
"layer 2 caching",
"distributed caching",
"keyv",
"expiration",
"CacheableMemory",
"distributed sync",
"secondary store",
"primary store",
"cache statistics",
"layered caching",
"fault tolerant",
"in-memory cache",
"distributed cache",
"lru",
"multi-tier cache"
],
"files": [
"dist",
"LICENSE"
]
}
38 changes: 38 additions & 0 deletions packages/net/src/fetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import {type Cacheable, type CacheableOptions} from 'cacheable';
import {fetch as undiciFetch, type RequestInit, type Response as UndiciResponse} from 'undici';

export type FetchOptions = Omit<RequestInit, 'cache'> & {
cache: Cacheable;
};

/**
* Fetch data from a URL with optional request options.
* @param {string} url The URL to fetch.
* @param {FetchOptions} options Optional request options. The `cacheable` property is required and should be an
* instance of `Cacheable` or a `CacheableOptions` object.
* @returns {Promise<UndiciResponse>} The response from the fetch.
*/
export async function fetch(url: string, options: FetchOptions): Promise<UndiciResponse> {
if (!options.cache) {
throw new Error('Fetch options must include a cache instance or options.');
}

const fetchOptions: RequestInit = {
...options,
cache: 'no-cache',
};

return options.cache.getOrSet(url, async () => {
// Perform the fetch operation
const response = await undiciFetch(url, fetchOptions);
/* c8 ignore next 3 */
if (!response.ok) {
throw new Error(`Fetch failed with status ${response.status}`);
}

return response;
}) as Promise<UndiciResponse>;
}

export type Response = UndiciResponse;
export type {RequestInit as FetchRequestInit} from 'undici';
50 changes: 50 additions & 0 deletions packages/net/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import {Hookified, type HookifiedOptions} from 'hookified';
import {Cacheable, type CacheableOptions} from 'cacheable';
import {
fetch, type FetchOptions, type Response as FetchResponse, type FetchRequestInit,
} from './fetch.js';

export type CacheableNetOptions = {
cache?: Cacheable | CacheableOptions;
} & HookifiedOptions;

export class CacheableNet extends Hookified {
private _cache: Cacheable = new Cacheable();

constructor(options?: CacheableNetOptions) {
super(options);

if (options?.cache) {
this._cache = options.cache instanceof Cacheable ? options.cache : new Cacheable(options.cache);
}
}

public get cache(): Cacheable {
return this._cache;
}

public set cache(value: Cacheable) {
this._cache = value;
}

/**
* Fetch data from a URL with optional request options. Will use the cache that is already set in the instance.
* @param {string} url The URL to fetch.
* @param {FetchRequestInit} options Optional request options.
* @returns {Promise<FetchResponse>} The response from the fetch.
*/
public async fetch(url: string, options?: FetchRequestInit): Promise<FetchResponse> {
const fetchOptions: FetchOptions = {
...options,
cache: this._cache,
};

return fetch(url, fetchOptions);
}
}

// eslint-disable-next-line @typescript-eslint/naming-convention
export const Net = CacheableNet;
export {
fetch, type FetchOptions, type Response as FetchResponse, type FetchRequestInit,
} from './fetch.js';
40 changes: 40 additions & 0 deletions packages/net/test/fetch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import process from 'node:process';
import {describe, test, expect} from 'vitest';
import {faker} from '@faker-js/faker';
import {Cacheable} from 'cacheable';
import {fetch, type FetchOptions} from '../src/fetch.js';

const testUrl = process.env.TEST_URL ?? 'https://mockhttp.org';
const testTimeout = 10_000; // 10 seconds

describe('Fetch', () => {
test('should fetch data successfully', async () => {
const url = `${testUrl}/get`;
const options: FetchOptions = {
method: 'GET',
cache: new Cacheable(),
};
const response = await fetch(url, options);
expect(response).toBeDefined();
}, testTimeout);

test('should fetch data successfully from cache', async () => {
const cache = new Cacheable({stats: true});
const url = `${testUrl}/get`;
const options: FetchOptions = {
method: 'GET',
cache,
};
const response = await fetch(url, options);
expect(response).toBeDefined();
}, testTimeout);

test('should throw an error if cache is not provided', async () => {
const url = `${testUrl}/get`;
const options: FetchOptions = {
method: 'GET',
cache: undefined as unknown as Cacheable, // Force error
};
await expect(fetch(url, options)).rejects.toThrow('Fetch options must include a cache instance or options.');
}, testTimeout);
});
75 changes: 75 additions & 0 deletions packages/net/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@

import process from 'node:process';
import {describe, test, expect} from 'vitest';
import {faker} from '@faker-js/faker';
import {Cacheable} from 'cacheable';
import {
CacheableNet, fetch, Net, type CacheableNetOptions, type FetchOptions,
} from '../src/index.js';

const testUrl = process.env.TEST_URL ?? 'https://mockhttp.org';
const testTimeout = 10_000; // 10 seconds

describe('Cacheable Net', () => {
test('should create an instance of CacheableNet', () => {
const net = new CacheableNet();
expect(net).toBeInstanceOf(CacheableNet);
});

test('should create an instance of Net', () => {
const net = new Net();
expect(net).toBeInstanceOf(CacheableNet);
});

test('should create an instance with cache instance', async () => {
const cacheOptions: CacheableNetOptions = {
cache: new Cacheable({ttl: '1h'}),
};
const net = new CacheableNet(cacheOptions);
expect(net.cache).toBeInstanceOf(Cacheable);
expect(net.cache.ttl).toBe('1h');

// Do a quick test to ensure the cache is working
const data = {key: faker.string.uuid(), value: faker.string.alpha(10)};
await net.cache.set(data.key, data.value);
const cachedValue = await net.cache.get(data.key);
expect(cachedValue).toBe(data.value);
});

test('should create an instance with custom cache options', () => {
const cacheOptions: CacheableNetOptions = {
cache: {
ttl: '2h',
},
};
const net = new Net(cacheOptions);
expect(net.cache).toBeInstanceOf(Cacheable);
expect(net.cache.ttl).toBe('2h');

// Set a new cache instance
const newCache = new Cacheable({ttl: '3h'});
net.cache = newCache;
expect(net.cache).toBe(newCache);
});

test('should fetch data using fetch method', async () => {
const cache = new Cacheable();
const url = `${testUrl}/get`;
const options: FetchOptions = {
method: 'GET',
cache,
};
const response = await fetch(url, options);
expect(response).toBeDefined();
});

test('should fetch data using CacheableNet fetch method', async () => {
const net = new Net();
const url = `${testUrl}/get`;
const options = {
method: 'GET',
};
const response = await net.fetch(url, options);
expect(response).toBeDefined();
}, testTimeout);
});
26 changes: 26 additions & 0 deletions packages/net/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
"baseUrl": "./src", /* Specify the base directory to resolve non-relative module names. */

/* Emit */
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
"outDir": "./dist", /* Specify an output folder for all emitted files. */

/* Interop Constraints */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */

/* Type Checking */
"strict": true, /* Enable all strict type-checking options. */

/* Completeness */
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
"lib": [
"ESNext", "DOM"
]
}
}
Loading