0
# Pagination & Connections
1
2
Relay-spec compliant pagination system with automatic page fetching, helper utilities, and efficient data loading for handling large datasets from the Linear API.
3
4
## Capabilities
5
6
### Connection-Based Pagination
7
8
Linear API uses Relay-specification pagination with connections and edges for efficient data handling.
9
10
```typescript { .api }
11
/**
12
* Enhanced connection class with pagination methods
13
*/
14
class Connection<Node> extends LinearConnection<Node> {
15
/** Pagination information */
16
pageInfo: PageInfo;
17
/** Array of nodes in the current page */
18
nodes: Node[];
19
20
/**
21
* Fetch the next page of results and append to existing nodes
22
* @returns This connection instance with additional nodes
23
*/
24
fetchNext(): Promise<this>;
25
26
/**
27
* Fetch the previous page of results and prepend to existing nodes
28
* @returns This connection instance with additional nodes
29
*/
30
fetchPrevious(): Promise<this>;
31
}
32
33
/**
34
* Base connection class providing core functionality
35
*/
36
class LinearConnection<Node> extends Request {
37
/** Pagination information */
38
pageInfo: PageInfo;
39
/** Array of nodes in the current page */
40
nodes: Node[];
41
42
constructor(request: LinearRequest);
43
}
44
45
/**
46
* Pagination information following Relay specification
47
*/
48
class PageInfo extends Request {
49
/** Whether there are more pages after this one */
50
hasNextPage: boolean;
51
/** Whether there are more pages before this one */
52
hasPreviousPage: boolean;
53
/** Cursor pointing to the start of this page */
54
startCursor?: string;
55
/** Cursor pointing to the end of this page */
56
endCursor?: string;
57
}
58
```
59
60
### Pagination Variables
61
62
Standard variables for controlling pagination behavior across all queries.
63
64
```typescript { .api }
65
/**
66
* Variables required for pagination following Relay spec
67
*/
68
interface LinearConnectionVariables {
69
/** Number of nodes to fetch after the cursor */
70
first?: number | null;
71
/** Number of nodes to fetch before the cursor */
72
last?: number | null;
73
/** Cursor to start fetching after */
74
after?: string | null;
75
/** Cursor to start fetching before */
76
before?: string | null;
77
}
78
```
79
80
**Usage Examples:**
81
82
```typescript
83
import { LinearClient } from "@linear/sdk";
84
85
const client = new LinearClient({ apiKey: "your-api-key" });
86
87
// Basic pagination - fetch first 20 issues
88
const issues = await client.issues({ first: 20 });
89
90
console.log(`Loaded ${issues.nodes.length} issues`);
91
console.log(`Has more pages: ${issues.pageInfo.hasNextPage}`);
92
93
// Load more pages manually
94
if (issues.pageInfo.hasNextPage) {
95
await issues.fetchNext();
96
console.log(`Now have ${issues.nodes.length} issues total`);
97
}
98
99
// Backward pagination
100
const recentIssues = await client.issues({ last: 10 });
101
102
// Cursor-based pagination
103
const page1 = await client.issues({ first: 25 });
104
const page2 = await client.issues({
105
first: 25,
106
after: page1.pageInfo.endCursor
107
});
108
```
109
110
### Auto-Pagination Helpers
111
112
Utility methods to automatically paginate through all pages of data.
113
114
```typescript { .api }
115
/**
116
* Base Request class providing pagination helpers
117
*/
118
class Request {
119
/**
120
* Helper to paginate over all pages of a given connection query
121
* Automatically fetches all pages and returns all nodes
122
* @param fn - The query function to paginate
123
* @param args - The arguments to pass to the query
124
* @returns Promise resolving to all nodes across all pages
125
*/
126
async paginate<T extends Node, U>(
127
fn: (variables: U) => LinearFetch<Connection<T>>,
128
args: U
129
): Promise<T[]>;
130
}
131
```
132
133
**Usage Examples:**
134
135
```typescript
136
// Fetch ALL issues for a team (across all pages)
137
const allTeamIssues = await client.paginate(
138
(vars) => client.issues(vars),
139
{
140
filter: {
141
team: { key: { eq: "ENG" } }
142
}
143
}
144
);
145
146
console.log(`Total team issues: ${allTeamIssues.length}`);
147
148
// Fetch ALL projects in the workspace
149
const allProjects = await client.paginate(
150
(vars) => client.projects(vars),
151
{}
152
);
153
154
// Fetch ALL comments for an issue
155
const allComments = await client.paginate(
156
(vars) => client.comments(vars),
157
{
158
filter: {
159
issue: { id: { eq: "issue-id" } }
160
}
161
}
162
);
163
```
164
165
### Efficient Pagination Patterns
166
167
**Load More Pattern:**
168
169
```typescript
170
class IssueList {
171
private client: LinearClient;
172
private currentConnection: IssueConnection | null = null;
173
174
constructor(client: LinearClient) {
175
this.client = client;
176
}
177
178
async loadInitial(teamKey: string): Promise<Issue[]> {
179
this.currentConnection = await this.client.issues({
180
first: 25,
181
filter: {
182
team: { key: { eq: teamKey } }
183
}
184
});
185
return this.currentConnection.nodes;
186
}
187
188
async loadMore(): Promise<Issue[]> {
189
if (!this.currentConnection?.pageInfo.hasNextPage) {
190
return [];
191
}
192
193
await this.currentConnection.fetchNext();
194
return this.currentConnection.nodes;
195
}
196
197
get hasMore(): boolean {
198
return this.currentConnection?.pageInfo.hasNextPage ?? false;
199
}
200
201
get totalLoaded(): number {
202
return this.currentConnection?.nodes.length ?? 0;
203
}
204
}
205
206
// Usage
207
const issueList = new IssueList(client);
208
const initialIssues = await issueList.loadInitial("ENG");
209
210
while (issueList.hasMore) {
211
console.log(`Loaded ${issueList.totalLoaded} issues so far`);
212
const moreIssues = await issueList.loadMore();
213
if (moreIssues.length === 0) break;
214
}
215
```
216
217
**Batch Processing Pattern:**
218
219
```typescript
220
async function processAllIssuesInBatches<T>(
221
client: LinearClient,
222
filter: IssueFilter,
223
processor: (batch: Issue[]) => Promise<T[]>,
224
batchSize = 50
225
): Promise<T[]> {
226
const results: T[] = [];
227
let connection = await client.issues({ first: batchSize, filter });
228
229
do {
230
// Process current batch
231
const batchResults = await processor(connection.nodes);
232
results.push(...batchResults);
233
234
console.log(`Processed ${connection.nodes.length} issues, total: ${results.length}`);
235
236
// Fetch next batch if available
237
if (connection.pageInfo.hasNextPage) {
238
await connection.fetchNext();
239
} else {
240
break;
241
}
242
} while (true);
243
244
return results;
245
}
246
247
// Usage - process all team issues in batches
248
const processedData = await processAllIssuesInBatches(
249
client,
250
{ team: { key: { eq: "ENG" } } },
251
async (issues) => {
252
// Process each batch (e.g., send to analytics, update external system)
253
return issues.map(issue => ({
254
id: issue.id,
255
title: issue.title,
256
processed: true
257
}));
258
}
259
);
260
```
261
262
### Performance Optimization
263
264
**Connection Caching:**
265
266
```typescript
267
class CachedLinearClient {
268
private client: LinearClient;
269
private connectionCache = new Map<string, any>();
270
271
constructor(client: LinearClient) {
272
this.client = client;
273
}
274
275
async getIssuesWithCache(variables: IssuesQueryVariables, cacheKey?: string): Promise<IssueConnection> {
276
const key = cacheKey || JSON.stringify(variables);
277
278
if (this.connectionCache.has(key)) {
279
const cached = this.connectionCache.get(key);
280
// Check if cache is still valid (e.g., less than 5 minutes old)
281
if (Date.now() - cached.timestamp < 5 * 60 * 1000) {
282
return cached.connection;
283
}
284
}
285
286
const connection = await this.client.issues(variables);
287
this.connectionCache.set(key, {
288
connection,
289
timestamp: Date.now()
290
});
291
292
return connection;
293
}
294
295
clearCache(): void {
296
this.connectionCache.clear();
297
}
298
}
299
```
300
301
**Smart Pagination with Progress:**
302
303
```typescript
304
interface PaginationProgress {
305
currentPage: number;
306
totalLoaded: number;
307
hasMore: boolean;
308
estimatedTotal?: number;
309
}
310
311
class ProgressivePaginator<T> {
312
private connection: Connection<T> | null = null;
313
private onProgress?: (progress: PaginationProgress) => void;
314
315
constructor(onProgress?: (progress: PaginationProgress) => void) {
316
this.onProgress = onProgress;
317
}
318
319
async paginateWithProgress<U>(
320
queryFn: (variables: U) => Promise<Connection<T>>,
321
variables: U,
322
pageSize = 50
323
): Promise<T[]> {
324
let currentPage = 1;
325
this.connection = await queryFn({ ...variables, first: pageSize });
326
327
const reportProgress = () => {
328
if (this.onProgress && this.connection) {
329
this.onProgress({
330
currentPage,
331
totalLoaded: this.connection.nodes.length,
332
hasMore: this.connection.pageInfo.hasNextPage
333
});
334
}
335
};
336
337
reportProgress();
338
339
while (this.connection.pageInfo.hasNextPage) {
340
await this.connection.fetchNext();
341
currentPage++;
342
reportProgress();
343
344
// Optional: yield control to prevent blocking
345
await new Promise(resolve => setTimeout(resolve, 0));
346
}
347
348
return this.connection.nodes;
349
}
350
}
351
352
// Usage
353
const paginator = new ProgressivePaginator<Issue>((progress) => {
354
console.log(`Page ${progress.currentPage}: ${progress.totalLoaded} items loaded`);
355
});
356
357
const allIssues = await paginator.paginateWithProgress(
358
(vars) => client.issues(vars),
359
{ filter: { team: { key: { eq: "ENG" } } } }
360
);
361
```
362
363
## Pagination Type Definitions
364
365
```typescript { .api }
366
/** Promise wrapper for Linear API responses */
367
type LinearFetch<Response> = Promise<Response>;
368
369
/** Function type for making GraphQL requests */
370
type LinearRequest = <Response, Variables extends Record<string, unknown>>(
371
doc: DocumentNode,
372
variables?: Variables
373
) => Promise<Response>;
374
375
/** Base node interface for paginated entities */
376
interface Node {
377
id: string;
378
}
379
380
/** Common pagination ordering options */
381
enum PaginationOrderBy {
382
CreatedAt = "createdAt",
383
UpdatedAt = "updatedAt"
384
}
385
386
/** Filter direction for ordering */
387
enum PaginationSortOrder {
388
Asc = "asc",
389
Desc = "desc"
390
}
391
```
392
393
## Best Practices
394
395
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
396
2. **Implement loading states**: Always show loading indicators during pagination operations
397
3. **Handle errors gracefully**: Network failures during pagination should allow retry
398
4. **Cache connections when appropriate**: Avoid refetching the same data repeatedly
399
5. **Use auto-pagination sparingly**: Only use `paginate()` when you actually need all data
400
6. **Monitor performance**: Large datasets can impact memory usage and performance
401
7. **Implement progress indicators**: For large datasets, show users pagination progress
402
8. **Consider cursor stability**: Cursors may become invalid if underlying data changes significantly