Skip to content

Commit 5e87084

Browse files
cubehouseclaude
andcommitted
feat(types): self-contained FetchLike type so consumers w/o DOM lib compile
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 285396a commit 5e87084

4 files changed

Lines changed: 62 additions & 6 deletions

File tree

src/client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { InMemoryLruCache, ttlForPath, type Cache } from './cache';
22
import { DestinationsApi } from './ergonomic/destinations';
33
import { EntityHandle } from './ergonomic/entity';
44
import { RawClient } from './raw';
5-
import { Transport, type RetryConfig } from './transport';
5+
import { Transport, type FetchLike, type RetryConfig } from './transport';
66

77
const DEFAULT_BASE_URL = 'https://api.themeparks.wiki/v1';
88
const PACKAGE_VERSION = '7.0.0-alpha.0';
@@ -11,7 +11,7 @@ const DEFAULT_USER_AGENT = `themeparks-sdk-js/${PACKAGE_VERSION}`;
1111
export interface ThemeParksOptions {
1212
baseUrl?: string;
1313
userAgent?: string;
14-
fetch?: typeof globalThis.fetch;
14+
fetch?: FetchLike;
1515
timeoutMs?: number;
1616
retry?: Partial<RetryConfig>;
1717
cache?: Cache | false | { maxEntries?: number };

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export { ThemeParks, type ThemeParksOptions } from './client';
22
export { ApiError, NetworkError, RateLimitError, ThemeParksError, TimeoutError } from './errors';
33
export { InMemoryLruCache, type Cache } from './cache';
4+
export type { FetchLike, FetchLikeResponse, RetryConfig, TransportOptions } from './transport';
45
export type { Destinations, Entity, EntityChildren, EntityLive, EntitySchedule } from './raw';
56
export {
67
currentWaitTime,

src/transport.ts

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,36 @@
11
import { ApiError, NetworkError, RateLimitError, TimeoutError } from './errors';
22

3+
/**
4+
* Minimal fetch-like function signature covering only what the SDK uses.
5+
*
6+
* Defined locally so consumers without the DOM lib (e.g. Node-only projects
7+
* with `lib: ["ES2022"]` and older `@types/node`) don't pull `globalThis.fetch`,
8+
* `Response`, or `RequestInit` into their emitted `.d.ts` when they import
9+
* this SDK's types.
10+
*
11+
* At runtime the SDK still calls `globalThis.fetch` by default; this type is
12+
* purely a structural subset so any spec-compatible `fetch` implementation
13+
* (node's built-in, undici, whatwg-fetch, a user mock, etc.) satisfies it.
14+
*/
15+
export type FetchLike = (
16+
input: string | URL,
17+
init?: {
18+
method?: string;
19+
headers?: Record<string, string>;
20+
signal?: AbortSignal;
21+
body?: string;
22+
},
23+
) => Promise<FetchLikeResponse>;
24+
25+
export interface FetchLikeResponse {
26+
ok: boolean;
27+
status: number;
28+
statusText: string;
29+
headers: { get(name: string): string | null };
30+
json(): Promise<unknown>;
31+
text(): Promise<string>;
32+
}
33+
334
/**
435
* Retry configuration for the {@link Transport}.
536
*
@@ -29,7 +60,7 @@ export interface TransportOptions {
2960
* initial attempt, so `{ max: 3 }` permits up to 4 total calls.
3061
*/
3162
retry: RetryConfig;
32-
fetch: typeof globalThis.fetch;
63+
fetch: FetchLike;
3364
sleep?: (ms: number) => Promise<void>;
3465
}
3566

@@ -102,7 +133,7 @@ export class Transport {
102133
const timer = setTimeout(() => {
103134
controller.abort();
104135
}, this.opts.timeoutMs);
105-
let response: Response;
136+
let response: FetchLikeResponse;
106137
try {
107138
response = await this.opts.fetch(url, {
108139
method,
@@ -166,7 +197,7 @@ export class Transport {
166197
}
167198
}
168199

169-
private async safeParseBody(response: Response): Promise<unknown> {
200+
private async safeParseBody(response: FetchLikeResponse): Promise<unknown> {
170201
const ct = response.headers.get('content-type') ?? '';
171202
if (!ct.includes('application/json')) {
172203
try {

test/unit/transport.test.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect, vi } from 'vitest';
2-
import { Transport } from '../../src/transport';
2+
import { Transport, type FetchLike } from '../../src/transport';
33
import { ApiError, NetworkError, RateLimitError, TimeoutError } from '../../src/errors';
44

55
function jsonResponse(body: unknown, init: ResponseInit = {}): Response {
@@ -125,6 +125,30 @@ describe('Transport', () => {
125125
await expect(t.get('/x')).rejects.toMatchObject({ name: 'NetworkError' });
126126
});
127127

128+
it('accepts a custom FetchLike-typed fetch implementation', async () => {
129+
// Type-level exercise: assign to FetchLike without any DOM types in scope
130+
// of the signature. Runtime uses a small mock.
131+
const myFetch: FetchLike = (_input, _init) =>
132+
Promise.resolve({
133+
ok: true,
134+
status: 200,
135+
statusText: 'OK',
136+
headers: {
137+
get: (_name: string): string | null => 'application/json',
138+
},
139+
json: (): Promise<unknown> => Promise.resolve({ hello: 'world' }),
140+
text: (): Promise<string> => Promise.resolve('{"hello":"world"}'),
141+
});
142+
const t = new Transport({
143+
baseUrl: 'https://api.example/v1',
144+
userAgent: 'test/1',
145+
timeoutMs: 1000,
146+
retry: { max: 0, on429: true },
147+
fetch: myFetch,
148+
});
149+
await expect(t.get('/x')).resolves.toEqual({ hello: 'world' });
150+
});
151+
128152
it('throws TimeoutError when the request exceeds timeoutMs', async () => {
129153
const fetchFn = vi.fn().mockImplementation(
130154
(_url: string, init: RequestInit) =>

0 commit comments

Comments
 (0)