or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

comments-attachments.mdcore-client.mderror-handling.mdindex.mdissue-management.mdpagination-connections.mdproject-management.mdteam-user-management.mdwebhook-processing.mdworkflow-cycle-management.md

pagination-connections.mddocs/

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