diff --git a/src/core/body-utils.ts b/src/core/body-utils.ts index 8342f2c..76ca0f4 100644 --- a/src/core/body-utils.ts +++ b/src/core/body-utils.ts @@ -40,7 +40,7 @@ const hasFormData = typeof FormData !== 'undefined' const hasURLSearchParams = typeof URLSearchParams !== 'undefined' const hasReadableStream = typeof ReadableStream !== 'undefined' -function isBinaryBody(value: unknown): value is BodyInit { +export function isBinaryBody(value: unknown): value is BodyInit { if (value === null || value === undefined) return false if (typeof value === 'string') return false diff --git a/src/core/errors.ts b/src/core/errors.ts index 1af081d..c656683 100644 --- a/src/core/errors.ts +++ b/src/core/errors.ts @@ -104,18 +104,21 @@ export function enhanceError(error: any, url: string, context?: ErrorContext): T error.code === 'ETIMEDOUT' ? 'Request timed out - server may be slow' : null ].filter(Boolean) as string[] - enhanced.debug = () => { - console.group(`🚨 Network Error Debug`) - console.log('URL:', url) - console.log('Method:', enhanced.method) - console.log('Error:', error.message) - console.log('Error Code:', error.code) - console.log('Timestamp:', new Date(enhanced.timestamp!).toISOString()) - if (enhanced.attempt) console.log('Attempt:', enhanced.attempt) - if (enhanced.duration) console.log('Duration:', `${enhanced.duration}ms`) - console.log('Stack:', error.stack) - console.groupEnd() - } + Object.defineProperty(enhanced, 'debug', { + enumerable: false, + value: () => { + console.group(`🚨 Network Error Debug`) + console.log('URL:', url) + console.log('Method:', enhanced.method) + console.log('Error:', error.message) + console.log('Error Code:', error.code) + console.log('Timestamp:', new Date(enhanced.timestamp!).toISOString()) + if (enhanced.attempt) console.log('Attempt:', enhanced.attempt) + if (enhanced.duration) console.log('Duration:', `${enhanced.duration}ms`) + console.log('Stack:', error.stack) + console.groupEnd() + } + }) return enhanced } diff --git a/src/core/typed-fetch.ts b/src/core/typed-fetch.ts index e50c3e9..403ea9e 100644 --- a/src/core/typed-fetch.ts +++ b/src/core/typed-fetch.ts @@ -12,7 +12,7 @@ import { InterceptorChain } from './interceptors.js' import { RequestMetrics } from './metrics.js' import { OfflineHandler } from './offline-handler.js' import { createHttpError, enhanceError, type ErrorContext } from './errors.js' -import { prepareBodyPayload, resolveBodyArgs } from './body-utils.js' +import { isBinaryBody, prepareBodyPayload, resolveBodyArgs } from './body-utils.js' import { createResource, type ResourceBuilderOptions, @@ -282,6 +282,58 @@ export class RevolutionaryTypedFetch { // Deduplicate identical requests - return this.deduplicator.dedupe(cacheKey, async () => { + return this.deduplicator.dedupe(dedupeKey, async () => { // Execute with circuit breaker and retry logic return this.executeWithRetry(fullUrl, processedOptions, url, method) }) diff --git a/tests/entrypoint-smoke.test.ts b/tests/entrypoint-smoke.test.ts new file mode 100644 index 0000000..6d73a92 --- /dev/null +++ b/tests/entrypoint-smoke.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from 'vitest' + +import * as entry from '../src/index.js' + +describe('package entrypoint exports', () => { + it('exposes the primary factory and singleton', () => { + expect(entry.tf).toBeDefined() + expect(typeof entry.createTypedFetch).toBe('function') + }) + + it('exposes key utilities for advanced users', () => { + expect(entry.RuntimeTypeInference).toBeDefined() + expect(entry.WTinyLFUCache).toBeDefined() + expect(entry.TypeDeclarationGenerator).toBeDefined() + }) +}) diff --git a/tests/typed-fetch-request.test.ts b/tests/typed-fetch-request.test.ts new file mode 100644 index 0000000..6716529 --- /dev/null +++ b/tests/typed-fetch-request.test.ts @@ -0,0 +1,43 @@ +import { afterEach, describe, expect, it, vi } from 'vitest' + +import { RevolutionaryTypedFetch } from '../src/core/typed-fetch.js' + +const jsonResponse = (body: any) => + new Response(JSON.stringify(body), { + headers: { 'Content-Type': 'application/json' } + }) + +describe('RevolutionaryTypedFetch request polish', () => { + afterEach(() => { + vi.restoreAllMocks() + }) + + it('applies a JSON content-type automatically for plain object bodies', async () => { + const fetchSpy = vi.fn(async (_url, init) => { + const headers = new Headers(init?.headers as HeadersInit) + expect(headers.get('content-type')).toBe('application/json') + return jsonResponse({ ok: true }) + }) + vi.stubGlobal('fetch', fetchSpy) + + const client = new RevolutionaryTypedFetch({ request: { baseURL: 'https://api.test' } }) + await client.post('/items', { name: 'test' }) + + expect(fetchSpy).toHaveBeenCalledTimes(1) + }) + + it('does not dedupe concurrent requests when bodies differ', async () => { + const fetchSpy = vi.fn(async (_url, init) => { + return jsonResponse({ echoed: init?.body }) + }) + vi.stubGlobal('fetch', fetchSpy) + + const client = new RevolutionaryTypedFetch({ request: { baseURL: 'https://api.test' } }) + await Promise.all([ + client.post('/items', { name: 'one' }), + client.post('/items', { name: 'two' }) + ]) + + expect(fetchSpy).toHaveBeenCalledTimes(2) + }) +})