diff --git a/core/api-doc-config.generated.json b/core/api-doc-config.generated.json index b3a0a72d..aed18ea4 100644 --- a/core/api-doc-config.generated.json +++ b/core/api-doc-config.generated.json @@ -1,5 +1,5 @@ { - "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:01:55.819Z. Do not edit manually.", + "_generated": "Auto-generated by extract-jsdoc.js on 2026-06-08T08:08:35.042Z. Do not edit manually.", "methods": { "has": { "summary": "HTTP verb for the endpoint (e.g. GET, POST). */", @@ -1011,4 +1011,4 @@ "python": "import pmxt\nimport os\n\nexchange = pmxt.Polymarket(\n private_key=os.getenv('POLYMARKET_PRIVATE_KEY')\n)\n\n# 1. Check balance\nbalances = exchange.fetch_balance()\nif balances:\n balance = balances[0]\n print(f'Available: ${balance.available}')\n\n# 2. Search for a market\nmarkets = exchange.fetch_markets(query='Trump')\nmarket = markets[0]\noutcome = market.yes\n\nprint(f'{market.title}')\nprint(f'Price: {outcome.price * 100:.1f}%')\n\n# 3. Place a limit order\norder = exchange.create_order(\n market_id=market.market_id,\n outcome_id=outcome.outcome_id,\n side='buy',\n type='limit',\n amount=10,\n price=0.50\n)\n\nprint(f'Order placed: {order.id}')\n\n# 4. Check order status\nupdated_order = exchange.fetch_order(order.id)\nprint(f'Status: {updated_order.status}')\nprint(f'Filled: {updated_order.filled}/{updated_order.amount}')\n\n# 5. Cancel if needed\nif updated_order.status == 'open':\n exchange.cancel_order(order.id)\n print('Order cancelled')\n\n# 6. Check positions\npositions = exchange.fetch_positions()\nfor pos in positions:\n pnl_sign = '+' if pos.unrealized_pnl > 0 else ''\n print(f'{pos.outcome_label}: {pnl_sign}${pos.unrealized_pnl:.2f}')", "typescript": "import pmxt from 'pmxtjs';\n\nconst exchange = new pmxt.Polymarket({\n privateKey: process.env.POLYMARKET_PRIVATE_KEY\n});\n\n// 1. Check balance\nconst [balance] = await exchange.fetchBalance();\nconsole.log(`Available: $${balance.available}`);\n\n// 2. Search for a market\nconst markets = await exchange.fetchMarkets({ query: 'Trump' });\nconst market = markets[0];\nconst outcome = market.yes;\n\nconsole.log(market.title);\nconsole.log(`Price: ${(outcome.price * 100).toFixed(1)}%`);\n\n// 3. Place a limit order\nconst order = await exchange.createOrder({\n marketId: market.marketId,\n outcomeId: outcome.outcomeId,\n side: 'buy',\n type: 'limit',\n amount: 10,\n price: 0.50\n});\n\nconsole.log(`Order placed: ${order.id}`);\n\n// 4. Check order status\nconst updatedOrder = await exchange.fetchOrder(order.id);\nconsole.log(`Status: ${updatedOrder.status}`);\nconsole.log(`Filled: ${updatedOrder.filled}/${updatedOrder.amount}`);\n\n// 5. Cancel if needed\nif (updatedOrder.status === 'open') {\n await exchange.cancelOrder(order.id);\n console.log('Order cancelled');\n}\n\n// 6. Check positions\nconst positions = await exchange.fetchPositions();\npositions.forEach(pos => {\n console.log(`${pos.outcomeLabel}: ${pos.unrealizedPnL > 0 ? '+' : ''}$${pos.unrealizedPnL.toFixed(2)}`);\n});" } -} +} \ No newline at end of file diff --git a/core/src/exchanges/limitless/fetcher.ts b/core/src/exchanges/limitless/fetcher.ts index aca7f6bf..01819d9a 100644 --- a/core/src/exchanges/limitless/fetcher.ts +++ b/core/src/exchanges/limitless/fetcher.ts @@ -102,6 +102,7 @@ export class LimitlessFetcher implements IExchangeFetcher { const { HttpClient, MarketFetcher } = await import('@limitless-exchange/sdk'); - const httpClient = new HttpClient({ baseURL: this.apiUrl }); + const httpClient = new HttpClient({ baseURL: this.apiUrl, timeout: 30000 }); const marketFetcher = new MarketFetcher(httpClient); const market = await marketFetcher.getMarket(slug); diff --git a/core/src/server/test-server.js b/core/src/server/test-server.js index 4b8a6454..e1975ba6 100644 --- a/core/src/server/test-server.js +++ b/core/src/server/test-server.js @@ -6,24 +6,32 @@ */ const BASE_URL = 'http://localhost:3847'; +const REQUEST_TIMEOUT_MS = 30_000; + +async function fetchWithTimeout(url, options = {}) { + return fetch(url, { + ...options, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }); +} async function testHealthCheck() { console.log('\nTesting health check...'); - const response = await fetch(`${BASE_URL}/health`); + const response = await fetchWithTimeout(`${BASE_URL}/health`); const data = await response.json(); console.log('Health check:', data); } async function testVersion() { console.log('\nTesting version endpoint...'); - const response = await fetch(`${BASE_URL}/version`); + const response = await fetchWithTimeout(`${BASE_URL}/version`); const data = await response.json(); console.log('Version:', data); } async function testFetchMarkets() { console.log('\nTesting fetchMarkets (Polymarket)...'); - const response = await fetch(`${BASE_URL}/api/polymarket/fetchMarkets`, { + const response = await fetchWithTimeout(`${BASE_URL}/api/polymarket/fetchMarkets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -42,7 +50,7 @@ async function testFetchMarkets() { async function testSearchMarkets() { console.log('\nTesting searchMarkets (Kalshi)...'); - const response = await fetch(`${BASE_URL}/api/kalshi/searchMarkets`, { + const response = await fetchWithTimeout(`${BASE_URL}/api/kalshi/searchMarkets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -63,7 +71,7 @@ async function testSearchMarkets() { async function testGetMarketsBySlug() { console.log('\nTesting getMarketsBySlug (Polymarket)...'); - const response = await fetch(`${BASE_URL}/api/polymarket/getMarketsBySlug`, { + const response = await fetchWithTimeout(`${BASE_URL}/api/polymarket/getMarketsBySlug`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -84,7 +92,7 @@ async function testFetchOHLCV() { console.log('\nTesting fetchOHLCV (Polymarket)...'); // First, get a market to extract an outcome ID - const marketsResponse = await fetch(`${BASE_URL}/api/polymarket/searchMarkets`, { + const marketsResponse = await fetchWithTimeout(`${BASE_URL}/api/polymarket/searchMarkets`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -101,7 +109,7 @@ async function testFetchOHLCV() { const outcomeId = marketsData.data[0].outcomes[0].id; console.log(` Using outcome ID: ${outcomeId.substring(0, 20)}...`); - const response = await fetch(`${BASE_URL}/api/polymarket/fetchOHLCV`, { + const response = await fetchWithTimeout(`${BASE_URL}/api/polymarket/fetchOHLCV`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/core/src/utils/throttler.ts b/core/src/utils/throttler.ts index 7673f5e1..740e4fc9 100644 --- a/core/src/utils/throttler.ts +++ b/core/src/utils/throttler.ts @@ -22,12 +22,10 @@ export class Throttler { } async throttle(cost: number = 1): Promise { - return new Promise((resolve) => { + return new Promise((resolve, reject) => { if (this.queue.length >= this.maxQueueDepth) { - const dropped = this.queue.shift(); - if (dropped) { - dropped.resolve(); - } + reject(new Error(`Throttler queue full (max depth ${this.maxQueueDepth})`)); + return; } this.queue.push({ resolve, cost }); if (!this.running) { diff --git a/core/test/utils/throttler.test.ts b/core/test/utils/throttler.test.ts new file mode 100644 index 00000000..517be987 --- /dev/null +++ b/core/test/utils/throttler.test.ts @@ -0,0 +1,24 @@ +import { Throttler } from '../../src/utils/throttler'; + +describe('Throttler', () => { + it('rejects new requests instead of silently resolving the oldest queued waiter', async () => { + const throttler = new Throttler({ + refillRate: 1, + capacity: 1, + delay: 1, + maxQueueDepth: 1, + }); + + let oldestResolved = false; + (throttler as any).queue.push({ + resolve: () => { + oldestResolved = true; + }, + cost: 1, + }); + + await expect(throttler.throttle(1)).rejects.toThrow(/queue full/i); + expect(oldestResolved).toBe(false); + expect((throttler as any).queue).toHaveLength(1); + }); +});