fix: guard pagination limits (#44) (thanks @jsholmes)

This commit is contained in:
Peter Steinberger
2026-01-12 07:07:47 +00:00
parent bc744a3bf9
commit afa7063a72
5 changed files with 79 additions and 5 deletions
+1
View File
@@ -11,6 +11,7 @@
- Long-form article tweets now render rich Draft.js content blocks/entities (#36) — thanks @crcatala.
- `news`/`trending` command for Explore tabs with AI-curated headlines (#39) — thanks @aavetis.
- `search` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#42) — thanks @pjtf93.
- `likes` now supports pagination (`--all`, `--max-pages`, `--cursor`) (#44) — thanks @jsholmes.
### Changed
- Library typing: `SearchResult` is now a discriminated union (so `error` only exists when `success: false`).
+1 -1
View File
@@ -449,7 +449,7 @@ export function withLists<TBase extends AbstractConstructor<TwitterClientBase>>(
return { success: false as const, error: firstAttempt.error };
};
const unlimited = !Number.isFinite(limit);
const unlimited = limit === Number.POSITIVE_INFINITY;
while (unlimited || tweets.length < limit) {
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
const page = await fetchWithRefresh(pageCount, cursor);
+1 -1
View File
@@ -209,7 +209,7 @@ export function withSearch<TBase extends AbstractConstructor<TwitterClientBase>>
return { success: false as const, error: firstAttempt.error };
};
const unlimited = !Number.isFinite(limit);
const unlimited = limit === Number.POSITIVE_INFINITY;
while (unlimited || tweets.length < limit) {
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
const page = await fetchWithRefresh(pageCount, cursor);
+3 -3
View File
@@ -204,7 +204,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
return { success: false as const, error: firstAttempt.error };
};
const unlimited = !Number.isFinite(limit);
const unlimited = limit === Number.POSITIVE_INFINITY;
while (unlimited || tweets.length < limit) {
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
const page = await fetchWithRefresh(pageCount, cursor);
@@ -382,7 +382,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
return { success: false as const, error: firstAttempt.error };
};
const unlimited = !Number.isFinite(limit);
const unlimited = limit === Number.POSITIVE_INFINITY;
while (unlimited || tweets.length < limit) {
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
const page = await fetchWithRefresh(pageCount, cursor);
@@ -565,7 +565,7 @@ export function withTimelines<TBase extends AbstractConstructor<TwitterClientBas
return { success: false as const, error: firstAttempt.error };
};
const unlimited = !Number.isFinite(limit);
const unlimited = limit === Number.POSITIVE_INFINITY;
while (unlimited || tweets.length < limit) {
const pageCount = unlimited ? pageSize : Math.min(pageSize, limit - tweets.length);
const page = await fetchWithRefresh(pageCount, cursor);
+73
View File
@@ -218,6 +218,79 @@ describe('TwitterClient likes', () => {
expect(secondVars.cursor).toBe('cursor-1');
});
it('respects maxPages when fetching all likes', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
data: {
user: {
result: {
timeline: {
timeline: {
instructions: [
{
entries: [
{
content: {
itemContent: {
tweet_results: {
result: {
rest_id: '1',
legacy: {
full_text: 'liked page 1',
created_at: '2024-01-01T00:00:00Z',
reply_count: 0,
retweet_count: 0,
favorite_count: 0,
conversation_id_str: '1',
},
core: {
user_results: {
result: {
rest_id: 'u1',
legacy: { screen_name: 'root', name: 'Root' },
},
},
},
},
},
},
},
},
{
content: {
cursorType: 'Bottom',
value: 'cursor-1',
},
},
],
},
],
},
},
},
},
},
}),
});
const client = new TwitterClient({ cookies: validCookies });
const clientPrivate = client as unknown as TwitterClientPrivate;
clientPrivate.getCurrentUser = async () => ({
success: true,
user: { id: '42', username: 'tester', name: 'Tester' },
});
clientPrivate.getLikesQueryIds = async () => ['test'];
const result = await client.getAllLikes({ maxPages: 1 });
expect(result.success).toBe(true);
expect(result.tweets?.map((tweet) => tweet.id)).toEqual(['1']);
expect(result.nextCursor).toBe('cursor-1');
expect(mockFetch).toHaveBeenCalledTimes(1);
});
it('returns an error when current user is unavailable', async () => {
const client = new TwitterClient({ cookies: validCookies });
const clientPrivate = client as unknown as TwitterClientPrivate;