-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor createClient function to support multiple API endpoints
- Loading branch information
Showing
1 changed file
with
98 additions
and
54 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,71 +1,115 @@ | ||
import { TradingAPI } from "../api/trading.ts"; | ||
import marketData from "../api/marketData.ts"; | ||
import trade from "../api/trade.ts"; | ||
|
||
import { TokenBucketOptions, createTokenBucket } from "./createTokenBucket.ts"; | ||
|
||
type ClientOptions = { | ||
apiKey: string; | ||
apiSecret: string; | ||
export type Trade = ReturnType<typeof trade>; | ||
|
||
export type MarketData = ReturnType<typeof marketData>; | ||
|
||
// Infer the client type based on the base URL | ||
type ClientFactoryMap = { | ||
"https://paper-api.alpaca.markets": Trade; | ||
"https://data.alpaca.markets": MarketData; | ||
}; | ||
|
||
export type Client = Trade | MarketData; | ||
|
||
type RequestOptions = { | ||
method?: string; | ||
path: string; | ||
params?: Record<string, any>; | ||
data?: object; | ||
}; | ||
|
||
type CreateClientOptions = { | ||
keyId: string; | ||
secretKey: string; | ||
baseURL: string; | ||
tokenBucket?: TokenBucketOptions; | ||
}; | ||
|
||
type Endpoint = keyof TradingAPI; | ||
type Method = TradingAPI[Endpoint]["method"]; | ||
type Params<E extends Endpoint> = TradingAPI[E]["params"]; | ||
type Response<E extends Endpoint> = TradingAPI[E]["response"]; | ||
|
||
function createClient(options: ClientOptions) { | ||
const tokenBucket = options.tokenBucket | ||
? createTokenBucket(options.tokenBucket) | ||
: undefined; | ||
|
||
const call = async <E extends Endpoint>( | ||
method: Method, | ||
endpoint: E, | ||
params?: Params<E> | ||
): Promise<Response<E>> => { | ||
if (tokenBucket && !tokenBucket.take(1)) { | ||
throw new Error("Rate limit exceeded"); | ||
} | ||
export type ClientContext = { | ||
options: CreateClientOptions; | ||
request: <T>(options: RequestOptions) => Promise<T>; | ||
}; | ||
|
||
export function createClient<T extends keyof ClientFactoryMap>( | ||
options: CreateClientOptions & { baseURL: T } | ||
): ClientFactoryMap[T] { | ||
// Create a token bucket for rate limiting | ||
const bucket = createTokenBucket(options.tokenBucket); | ||
|
||
const url = new URL(`${options.baseURL}${endpoint}`); | ||
if (params && method === "GET") { | ||
Object.entries(params as Record<string, string>).forEach( | ||
([key, value]) => { | ||
url.searchParams.append(key, value); | ||
// Throttled request function that respects the token bucket | ||
const request = async <T>({ | ||
method = "GET", | ||
path, | ||
params, | ||
data, | ||
}: RequestOptions): Promise<T> => { | ||
await new Promise((resolve) => { | ||
// Poll the token bucket every second | ||
const timer = setInterval(() => { | ||
// If a token is available, resolve the promise | ||
if (bucket.take(1)) { | ||
clearInterval(timer); | ||
resolve(true); | ||
} | ||
); | ||
}, 1000); | ||
}); | ||
|
||
// Hold the final path | ||
let modified = path; | ||
|
||
if (params) { | ||
// Replace path parameters with actual values | ||
for (const [key, value] of Object.entries(params)) { | ||
modified = modified.replace(`{${key}}`, encodeURIComponent(value)); | ||
} | ||
} | ||
|
||
const response = await fetch(url.toString(), { | ||
// Construct the full URL | ||
const url = `${options.baseURL}${modified}`; | ||
|
||
// Construct the headers | ||
const headers = new Headers({ | ||
"APCA-API-KEY-ID": options.keyId, | ||
"APCA-API-SECRET-KEY": options.secretKey, | ||
"Content-Type": "application/json", | ||
}); | ||
|
||
// Make the request and parse the JSON response | ||
return fetch(url, { | ||
method, | ||
headers: { | ||
"APCA-API-KEY-ID": options.apiKey, | ||
"APCA-API-SECRET-KEY": options.apiSecret, | ||
"Content-Type": "application/json", | ||
}, | ||
body: method !== "GET" ? JSON.stringify(params) : undefined, | ||
headers, | ||
body: data ? JSON.stringify(data) : null, | ||
}).then((response) => { | ||
if (!response.ok) { | ||
throw new Error( | ||
`Failed to ${method} ${url}: ${response.status} ${response.statusText}` | ||
); | ||
} | ||
|
||
return response.json(); | ||
}); | ||
}; | ||
|
||
if (!response.ok) { | ||
throw new Error(`HTTP error! status: ${response.status}`); | ||
} | ||
// Create a context object to pass to the client factory | ||
const context: ClientContext = { | ||
options, | ||
request, | ||
}; | ||
|
||
return (await response.json()) as Response<E>; | ||
// Conditonally return client based on the base URL | ||
const factory = (context: ClientContext): ClientFactoryMap[T] => { | ||
if (options.baseURL === "https://paper-api.alpaca.markets") { | ||
return trade(context) as ClientFactoryMap[T]; | ||
} else if (options.baseURL === "https://data.alpaca.markets") { | ||
return marketData(context) as ClientFactoryMap[T]; | ||
} else { | ||
throw new Error("invalid base URL"); | ||
} | ||
}; | ||
|
||
return { call }; | ||
return factory(context); | ||
} | ||
|
||
const client = createClient({ | ||
apiKey: "your-api-key", | ||
apiSecret: "your-api-secret", | ||
baseURL: "https://api.example.com", | ||
tokenBucket: { | ||
capacity: 200, | ||
fillRate: 3, | ||
}, | ||
}); | ||
|
||
client.call("GET", "/v2/account/activities").then((response) => { | ||
console.log(response); | ||
}); |