Skip to content

feat(pyth-lazer-sdk): Add AbortSignal and timeout support to PythLazerClient.create() #3543

@pythia-assistant

Description

@pythia-assistant

Problem

PythLazerClient.create() blocks indefinitely when WebSocket endpoints are unreachable. The WebSocketPool.create() method contains this loop:

while(!pool.isAnyConnectionEstablished()) {
  await new Promise((resolve) => setTimeout(resolve, 100));
}

This causes issues for fault-tolerant services (e.g., relayers) that need to:

  • Handle Pyth Pro being temporarily unavailable
  • Implement graceful degradation
  • Avoid hanging promises that block service startup

Proposed Solution

Add optional AbortSignal and connectionTimeoutMs parameters to WebSocketPoolConfig:

export type WebSocketPoolConfig = {
  // ... existing fields ...
  
  /**
   * AbortSignal to cancel connection establishment.
   * If aborted before connection is established, create() will throw.
   */
  signal?: AbortSignal;
  
  /**
   * Timeout in ms for initial connection establishment.
   * If not connected within this time, create() will throw.
   * @defaultValue Infinity (no timeout)
   */
  connectionTimeoutMs?: number;
};

Implementation

In WebSocketPool.create():

static async create(config: WebSocketPoolConfig, token: string, logger?: Logger): Promise<WebSocketPool> {
  // ... existing setup code ...
  
  const startTime = Date.now();
  const timeoutMs = config.connectionTimeoutMs ?? Infinity;
  
  while(!pool.isAnyConnectionEstablished()) {
    // Check abort signal
    if (config.signal?.aborted) {
      pool.shutdown();
      throw new DOMException('Connection aborted', 'AbortError');
    }
    
    // Check timeout
    if (Date.now() - startTime > timeoutMs) {
      pool.shutdown();
      throw new Error(`Connection timeout: failed to establish connection within ${timeoutMs}ms`);
    }
    
    await new Promise((resolve) => setTimeout(resolve, 100));
  }
  
  // ... rest of method ...
}

Usage Examples

With AbortSignal

const controller = new AbortController();

// Abort after 5 seconds
setTimeout(() => controller.abort(), 5000);

try {
  const client = await PythLazerClient.create({
    token: process.env.ACCESS_TOKEN!,
    webSocketPoolConfig: {
      urls: [...],
      signal: controller.signal
    }
  });
} catch (err) {
  if (err.name === 'AbortError') {
    console.log('Connection was aborted');
    // Handle gracefully - e.g., continue without real-time prices
  }
}

With timeout (simpler API)

try {
  const client = await PythLazerClient.create({
    token: process.env.ACCESS_TOKEN!,
    webSocketPoolConfig: {
      urls: [...],
      connectionTimeoutMs: 5000
    }
  });
} catch (err) {
  console.log('Connection timed out, falling back to REST API');
}

Current workaround

// Users currently have to do this manually:
const timeout = (ms) => new Promise((_, reject) => 
  setTimeout(() => reject(new Error('timeout')), ms)
);

const client = await Promise.race([
  PythLazerClient.create({ token, webSocketPoolConfig: { urls } }),
  timeout(5000)
]);
// Problem: WebSocket connections keep running in background after timeout

Use Case

A relayer service that needs high fault tolerance reported this issue. When Pyth Pro endpoints are temporarily unavailable, their service hangs on startup instead of gracefully degrading to alternative data sources.

This is a standard pattern for async operations in modern JavaScript/TypeScript — fetch(), ReadableStream, and many other APIs support AbortSignal for cancellation.

Additional Context

  • SDK version: 6.0.0
  • The AbortSignal pattern aligns with web platform standards
  • Both options (signal and connectionTimeoutMs) provide flexibility for different use cases

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions