CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-linear--sdk

The Linear Client SDK for interacting with the Linear GraphQL API

Overview
Eval results
Files

pagination-connections.mddocs/

Pagination & Connections

Relay-spec compliant pagination system with automatic page fetching, helper utilities, and efficient data loading for handling large datasets from the Linear API.

Capabilities

Connection-Based Pagination

Linear API uses Relay-specification pagination with connections and edges for efficient data handling.

/**
 * Enhanced connection class with pagination methods
 */
class Connection<Node> extends LinearConnection<Node> {
  /** Pagination information */
  pageInfo: PageInfo;
  /** Array of nodes in the current page */
  nodes: Node[];

  /**
   * Fetch the next page of results and append to existing nodes
   * @returns This connection instance with additional nodes
   */
  fetchNext(): Promise<this>;

  /**
   * Fetch the previous page of results and prepend to existing nodes
   * @returns This connection instance with additional nodes
   */
  fetchPrevious(): Promise<this>;
}

/**
 * Base connection class providing core functionality
 */
class LinearConnection<Node> extends Request {
  /** Pagination information */
  pageInfo: PageInfo;
  /** Array of nodes in the current page */
  nodes: Node[];

  constructor(request: LinearRequest);
}

/**
 * Pagination information following Relay specification
 */
class PageInfo extends Request {
  /** Whether there are more pages after this one */
  hasNextPage: boolean;
  /** Whether there are more pages before this one */
  hasPreviousPage: boolean;
  /** Cursor pointing to the start of this page */
  startCursor?: string;
  /** Cursor pointing to the end of this page */
  endCursor?: string;
}

Pagination Variables

Standard variables for controlling pagination behavior across all queries.

/**
 * Variables required for pagination following Relay spec
 */
interface LinearConnectionVariables {
  /** Number of nodes to fetch after the cursor */
  first?: number | null;
  /** Number of nodes to fetch before the cursor */
  last?: number | null;
  /** Cursor to start fetching after */
  after?: string | null;
  /** Cursor to start fetching before */
  before?: string | null;
}

Usage Examples:

import { LinearClient } from "@linear/sdk";

const client = new LinearClient({ apiKey: "your-api-key" });

// Basic pagination - fetch first 20 issues
const issues = await client.issues({ first: 20 });

console.log(`Loaded ${issues.nodes.length} issues`);
console.log(`Has more pages: ${issues.pageInfo.hasNextPage}`);

// Load more pages manually
if (issues.pageInfo.hasNextPage) {
  await issues.fetchNext();
  console.log(`Now have ${issues.nodes.length} issues total`);
}

// Backward pagination
const recentIssues = await client.issues({ last: 10 });

// Cursor-based pagination
const page1 = await client.issues({ first: 25 });
const page2 = await client.issues({
  first: 25,
  after: page1.pageInfo.endCursor
});

Auto-Pagination Helpers

Utility methods to automatically paginate through all pages of data.

/**
 * Base Request class providing pagination helpers
 */
class Request {
  /**
   * Helper to paginate over all pages of a given connection query
   * Automatically fetches all pages and returns all nodes
   * @param fn - The query function to paginate
   * @param args - The arguments to pass to the query
   * @returns Promise resolving to all nodes across all pages
   */
  async paginate<T extends Node, U>(
    fn: (variables: U) => LinearFetch<Connection<T>>,
    args: U
  ): Promise<T[]>;
}

Usage Examples:

// Fetch ALL issues for a team (across all pages)
const allTeamIssues = await client.paginate(
  (vars) => client.issues(vars),
  {
    filter: {
      team: { key: { eq: "ENG" } }
    }
  }
);

console.log(`Total team issues: ${allTeamIssues.length}`);

// Fetch ALL projects in the workspace
const allProjects = await client.paginate(
  (vars) => client.projects(vars),
  {}
);

// Fetch ALL comments for an issue
const allComments = await client.paginate(
  (vars) => client.comments(vars),
  {
    filter: {
      issue: { id: { eq: "issue-id" } }
    }
  }
);

Efficient Pagination Patterns

Load More Pattern:

class IssueList {
  private client: LinearClient;
  private currentConnection: IssueConnection | null = null;

  constructor(client: LinearClient) {
    this.client = client;
  }

  async loadInitial(teamKey: string): Promise<Issue[]> {
    this.currentConnection = await this.client.issues({
      first: 25,
      filter: {
        team: { key: { eq: teamKey } }
      }
    });
    return this.currentConnection.nodes;
  }

  async loadMore(): Promise<Issue[]> {
    if (!this.currentConnection?.pageInfo.hasNextPage) {
      return [];
    }

    await this.currentConnection.fetchNext();
    return this.currentConnection.nodes;
  }

  get hasMore(): boolean {
    return this.currentConnection?.pageInfo.hasNextPage ?? false;
  }

  get totalLoaded(): number {
    return this.currentConnection?.nodes.length ?? 0;
  }
}

// Usage
const issueList = new IssueList(client);
const initialIssues = await issueList.loadInitial("ENG");

while (issueList.hasMore) {
  console.log(`Loaded ${issueList.totalLoaded} issues so far`);
  const moreIssues = await issueList.loadMore();
  if (moreIssues.length === 0) break;
}

Batch Processing Pattern:

async function processAllIssuesInBatches<T>(
  client: LinearClient,
  filter: IssueFilter,
  processor: (batch: Issue[]) => Promise<T[]>,
  batchSize = 50
): Promise<T[]> {
  const results: T[] = [];
  let connection = await client.issues({ first: batchSize, filter });

  do {
    // Process current batch
    const batchResults = await processor(connection.nodes);
    results.push(...batchResults);

    console.log(`Processed ${connection.nodes.length} issues, total: ${results.length}`);

    // Fetch next batch if available
    if (connection.pageInfo.hasNextPage) {
      await connection.fetchNext();
    } else {
      break;
    }
  } while (true);

  return results;
}

// Usage - process all team issues in batches
const processedData = await processAllIssuesInBatches(
  client,
  { team: { key: { eq: "ENG" } } },
  async (issues) => {
    // Process each batch (e.g., send to analytics, update external system)
    return issues.map(issue => ({
      id: issue.id,
      title: issue.title,
      processed: true
    }));
  }
);

Performance Optimization

Connection Caching:

class CachedLinearClient {
  private client: LinearClient;
  private connectionCache = new Map<string, any>();

  constructor(client: LinearClient) {
    this.client = client;
  }

  async getIssuesWithCache(variables: IssuesQueryVariables, cacheKey?: string): Promise<IssueConnection> {
    const key = cacheKey || JSON.stringify(variables);

    if (this.connectionCache.has(key)) {
      const cached = this.connectionCache.get(key);
      // Check if cache is still valid (e.g., less than 5 minutes old)
      if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
        return cached.connection;
      }
    }

    const connection = await this.client.issues(variables);
    this.connectionCache.set(key, {
      connection,
      timestamp: Date.now()
    });

    return connection;
  }

  clearCache(): void {
    this.connectionCache.clear();
  }
}

Smart Pagination with Progress:

interface PaginationProgress {
  currentPage: number;
  totalLoaded: number;
  hasMore: boolean;
  estimatedTotal?: number;
}

class ProgressivePaginator<T> {
  private connection: Connection<T> | null = null;
  private onProgress?: (progress: PaginationProgress) => void;

  constructor(onProgress?: (progress: PaginationProgress) => void) {
    this.onProgress = onProgress;
  }

  async paginateWithProgress<U>(
    queryFn: (variables: U) => Promise<Connection<T>>,
    variables: U,
    pageSize = 50
  ): Promise<T[]> {
    let currentPage = 1;
    this.connection = await queryFn({ ...variables, first: pageSize });

    const reportProgress = () => {
      if (this.onProgress && this.connection) {
        this.onProgress({
          currentPage,
          totalLoaded: this.connection.nodes.length,
          hasMore: this.connection.pageInfo.hasNextPage
        });
      }
    };

    reportProgress();

    while (this.connection.pageInfo.hasNextPage) {
      await this.connection.fetchNext();
      currentPage++;
      reportProgress();

      // Optional: yield control to prevent blocking
      await new Promise(resolve => setTimeout(resolve, 0));
    }

    return this.connection.nodes;
  }
}

// Usage
const paginator = new ProgressivePaginator<Issue>((progress) => {
  console.log(`Page ${progress.currentPage}: ${progress.totalLoaded} items loaded`);
});

const allIssues = await paginator.paginateWithProgress(
  (vars) => client.issues(vars),
  { filter: { team: { key: { eq: "ENG" } } } }
);

Pagination Type Definitions

/** Promise wrapper for Linear API responses */
type LinearFetch<Response> = Promise<Response>;

/** Function type for making GraphQL requests */
type LinearRequest = <Response, Variables extends Record<string, unknown>>(
  doc: DocumentNode,
  variables?: Variables
) => Promise<Response>;

/** Base node interface for paginated entities */
interface Node {
  id: string;
}

/** Common pagination ordering options */
enum PaginationOrderBy {
  CreatedAt = "createdAt",
  UpdatedAt = "updatedAt"
}

/** Filter direction for ordering */
enum PaginationSortOrder {
  Asc = "asc",
  Desc = "desc"
}

Best Practices

  1. Use appropriate page sizes: Default is 50, but use smaller sizes (10-25) for real-time UI and larger sizes (100-250) for batch processing
  2. Implement loading states: Always show loading indicators during pagination operations
  3. Handle errors gracefully: Network failures during pagination should allow retry
  4. Cache connections when appropriate: Avoid refetching the same data repeatedly
  5. Use auto-pagination sparingly: Only use paginate() when you actually need all data
  6. Monitor performance: Large datasets can impact memory usage and performance
  7. Implement progress indicators: For large datasets, show users pagination progress
  8. Consider cursor stability: Cursors may become invalid if underlying data changes significantly

Install with Tessl CLI

npx tessl i tessl/npm-linear--sdk

docs

comments-attachments.md

core-client.md

error-handling.md

index.md

issue-management.md

pagination-connections.md

project-management.md

team-user-management.md

webhook-processing.md

workflow-cycle-management.md

tile.json