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
34 changes: 30 additions & 4 deletions packages/net/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,17 +63,43 @@ export async function fetch(
}) as UndiciResponse;
}

export type GetResponse<T = unknown> = {
data: T;
response: UndiciResponse;
};

/**
* Perform a GET request to a URL with optional request options.
* @param {string} url The URL to fetch.
* @param {Omit<FetchOptions, 'method'>} options Optional request options. The `cache` property is required.
* @returns {Promise<UndiciResponse>} The response from the fetch.
* @returns {Promise<GetResponse<T>>} The typed data and response from the fetch.
*/
export async function get(
export async function get<T = unknown>(
url: string,
options: Omit<FetchOptions, "method">,
): Promise<UndiciResponse> {
return fetch(url, { ...options, method: "GET" });
): Promise<GetResponse<T>> {
const response = await fetch(url, { ...options, method: "GET" });
const text = await response.text();
let data: T;

try {
data = JSON.parse(text) as T;
} catch {
// If not JSON, return as is
data = text as T;
}

// Create a new response with the text already consumed
const newResponse = new Response(text, {
status: response.status,
statusText: response.statusText,
headers: response.headers as HeadersInit,
}) as UndiciResponse;

return {
data,
response: newResponse,
};
}

export type Response = UndiciResponse;
Expand Down
31 changes: 27 additions & 4 deletions packages/net/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type FetchRequestInit,
type Response as FetchResponse,
fetch,
type GetResponse,
} from "./fetch.js";

export type CacheableNetOptions = {
Expand Down Expand Up @@ -55,13 +56,34 @@ export class CacheableNet extends Hookified {
* Perform a GET request to 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 {Omit<FetchRequestInit, 'method'>} options Optional request options (method will be set to GET).
* @returns {Promise<FetchResponse>} The response from the fetch.
* @returns {Promise<GetResponse<T>>} The typed data and response from the fetch.
*/
public async get(
public async get<T = unknown>(
url: string,
options?: Omit<FetchRequestInit, "method">,
): Promise<FetchResponse> {
return this.fetch(url, { ...options, method: "GET" });
): Promise<GetResponse<T>> {
const response = await this.fetch(url, { ...options, method: "GET" });
const text = await response.text();
let data: T;

try {
data = JSON.parse(text) as T;
} catch {
// If not JSON, return as is
data = text as T;
}

// Create a new response with the text already consumed
const newResponse = new Response(text, {
status: response.status,
statusText: response.statusText,
headers: response.headers as HeadersInit,
}) as FetchResponse;

return {
data,
response: newResponse,
};
}
}

Expand All @@ -70,6 +92,7 @@ export {
type FetchOptions,
type FetchRequestInit,
fetch,
type GetResponse,
get,
type Response as FetchResponse,
} from "./fetch.js";
62 changes: 51 additions & 11 deletions packages/net/test/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,24 @@ describe("Fetch", () => {
testTimeout,
);

test(
"should fetch data without method (defaults to GET)",
async () => {
const url = `${testUrl}/get`;
const cache = new Cacheable({ stats: true });
const options: FetchOptions = {
cache,
} as FetchOptions;
const response = await fetch(url, options);
expect(response).toBeDefined();
// Make second request to verify caching with default GET method
const response2 = await fetch(url, options);
expect(response2).toBeDefined();
expect(cache.stats.hits).toBe(1);
},
testTimeout,
);

test(
"should fetch data successfully from cache",
async () => {
Expand Down Expand Up @@ -66,9 +84,11 @@ describe("Fetch", () => {
const options = {
cache: new Cacheable(),
};
const response = await get(url, options);
expect(response).toBeDefined();
expect(response.status).toBe(200);
const result = await get(url, options);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
testTimeout,
);
Expand All @@ -81,16 +101,36 @@ describe("Fetch", () => {
const options = {
cache,
};
const response = await get(url, options);
const response2 = await get(url, options);
expect(response).toBeDefined();
expect(response2).toBeDefined();
const result1 = await get(url, options);
const result2 = await get(url, options);
expect(result1).toBeDefined();
expect(result2).toBeDefined();
expect(cache.stats).toBeDefined();
expect(cache.stats.hits).toBe(1);
// Verify that both responses have the same text content
const text1 = await response.text();
const text2 = await response2.text();
expect(text1).toEqual(text2);
// Verify that both responses have the same data
expect(result1.data).toEqual(result2.data);
// Verify response objects are valid
expect(result1.response.status).toBe(200);
expect(result2.response.status).toBe(200);
},
testTimeout,
);

test(
"should handle non-JSON response in get helper",
async () => {
const cache = new Cacheable();
// Mock a text response by using a URL that returns plain text
const mockTextUrl = "https://httpbin.org/robots.txt";
const options = {
cache,
};
const result = await get(mockTextUrl, options);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
testTimeout,
);
Expand Down
53 changes: 47 additions & 6 deletions packages/net/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ describe("Cacheable Net", () => {
async () => {
const net = new Net();
const url = `${testUrl}/get`;
const response = await net.get(url);
expect(response).toBeDefined();
expect(response.status).toBe(200);
const result = await net.get(url);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
testTimeout,
);
Expand All @@ -100,9 +102,48 @@ describe("Cacheable Net", () => {
const options = {
cache: new Cacheable(),
};
const response = await get(url, options);
expect(response).toBeDefined();
expect(response.status).toBe(200);
const result = await get(url, options);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
testTimeout,
);

test(
"should fetch typed data using get with generics",
async () => {
interface TestData {
method: string;
url: string;
}
const net = new Net();
const url = `${testUrl}/get`;
const result = await net.get<TestData>(url);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(result.response).toBeDefined();
// TypeScript will ensure result.data has the TestData type
if (typeof result.data === "object" && result.data !== null) {
expect(result.data).toHaveProperty("method");
}
},
testTimeout,
);

test(
"should handle non-JSON response in CacheableNet get method",
async () => {
const net = new Net();
// Use a URL that returns plain text
const mockTextUrl = "https://httpbin.org/robots.txt";
const result = await net.get(mockTextUrl);
expect(result).toBeDefined();
expect(result.data).toBeDefined();
expect(typeof result.data).toBe("string");
expect(result.response).toBeDefined();
expect(result.response.status).toBe(200);
},
testTimeout,
);
Expand Down