Skip to content

Commit 2d1611c

Browse files
authored
fix(api): split merged graphql enrichment into batches to limit errors (#2555)
Signed-off-by: Adam Setch <adam.setch@outlook.com>
1 parent 921765e commit 2d1611c

File tree

3 files changed

+125
-14
lines changed

3 files changed

+125
-14
lines changed

src/renderer/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const Constants = {
1818

1919
GITHUB_API_BASE_URL: 'https://api.github.com',
2020
GITHUB_API_GRAPHQL_URL: 'https://api.github.com/graphql',
21+
GITHUB_API_MERGE_BATCH_SIZE: 100,
2122

2223
ALL_READ_EMOJIS: ['🎉', '🎊', '🥳', '👏', '🙌', '😎', '🏖️', '🚀', '✨', '🏆'],
2324

src/renderer/utils/notifications/notifications.test.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import {
1212
} from '../../__mocks__/notifications-mocks';
1313
import { mockSettings } from '../../__mocks__/state-mocks';
1414

15+
import { Constants } from '../../constants';
16+
1517
import {
1618
type AccountNotifications,
1719
type GitifyNotification,
@@ -22,6 +24,7 @@ import {
2224
} from '../../types';
2325

2426
import * as logger from '../../utils/logger';
27+
import * as apiClient from '../api/client';
2528
import {
2629
enrichNotification,
2730
enrichNotifications,
@@ -190,5 +193,68 @@ describe('renderer/utils/notifications/notifications.ts', () => {
190193

191194
expect(result).toEqual([]);
192195
});
196+
197+
it('should batch notifications by GITHUB_API_MERGE_BATCH_SIZE', async () => {
198+
const fetchNotificationDetailsForListSpy = jest
199+
.spyOn(apiClient, 'fetchNotificationDetailsForList')
200+
.mockResolvedValue(new Map());
201+
202+
const notificationCount = Constants.GITHUB_API_MERGE_BATCH_SIZE * 2.5;
203+
const notifications = Array.from({ length: notificationCount }, (_, i) =>
204+
mockPartialGitifyNotification({
205+
title: `Notification ${i}`,
206+
type: 'Issue',
207+
url: `https://api.github.com/repos/gitify-app/notifications-test/issues/${i}` as Link,
208+
}),
209+
) as GitifyNotification[];
210+
211+
const settings: SettingsState = {
212+
...mockSettings,
213+
detailedNotifications: true,
214+
};
215+
216+
await enrichNotifications(notifications, settings);
217+
218+
// Should be called 3 times: batches of 100, 100, 50
219+
expect(fetchNotificationDetailsForListSpy).toHaveBeenCalledTimes(3);
220+
221+
// Verify batch sizes
222+
expect(fetchNotificationDetailsForListSpy.mock.calls[0][0]).toHaveLength(
223+
Constants.GITHUB_API_MERGE_BATCH_SIZE,
224+
);
225+
expect(fetchNotificationDetailsForListSpy.mock.calls[1][0]).toHaveLength(
226+
Constants.GITHUB_API_MERGE_BATCH_SIZE,
227+
);
228+
expect(fetchNotificationDetailsForListSpy.mock.calls[2][0]).toHaveLength(
229+
50,
230+
);
231+
});
232+
233+
it('should handle single batch of notifications', async () => {
234+
const fetchNotificationDetailsForListSpy = jest
235+
.spyOn(apiClient, 'fetchNotificationDetailsForList')
236+
.mockResolvedValue(new Map());
237+
238+
const notifications = Array.from({ length: 50 }, (_, i) =>
239+
mockPartialGitifyNotification({
240+
title: `Notification ${i}`,
241+
type: 'Issue',
242+
url: `https://api.github.com/repos/gitify-app/notifications-test/issues/${i}` as Link,
243+
}),
244+
) as GitifyNotification[];
245+
246+
const settings: SettingsState = {
247+
...mockSettings,
248+
detailedNotifications: true,
249+
};
250+
251+
await enrichNotifications(notifications, settings);
252+
253+
// Should be called once for single batch
254+
expect(fetchNotificationDetailsForListSpy).toHaveBeenCalledTimes(1);
255+
expect(fetchNotificationDetailsForListSpy.mock.calls[0][0]).toHaveLength(
256+
50,
257+
);
258+
});
193259
});
194260
});

src/renderer/utils/notifications/notifications.ts

Lines changed: 58 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Constants } from '../../constants';
2+
13
import type {
24
AccountNotifications,
35
GitifyNotification,
@@ -141,6 +143,13 @@ export async function getAllNotifications(
141143
return accountNotifications;
142144
}
143145

146+
/**
147+
* Enrich notification details
148+
*
149+
* @param notifications All Gitify inbox notifications
150+
* @param settings
151+
* @returns
152+
*/
144153
export async function enrichNotifications(
145154
notifications: GitifyNotification[],
146155
settings: SettingsState,
@@ -149,20 +158,7 @@ export async function enrichNotifications(
149158
return notifications;
150159
}
151160

152-
// Build and fetch merged details via client; returns per-notification results
153-
let mergedResults: Map<
154-
GitifyNotification,
155-
FetchMergedDetailsTemplateQuery['repository']
156-
> = new Map();
157-
try {
158-
mergedResults = await fetchNotificationDetailsForList(notifications);
159-
} catch (err) {
160-
rendererLogError(
161-
'enrichNotifications',
162-
'Failed to fetch merged notification details',
163-
err,
164-
);
165-
}
161+
const mergedResults = await fetchNotificationDetailsInBatches(notifications);
166162

167163
const enrichedNotifications = await Promise.all(
168164
notifications.map(async (notification: GitifyNotification) => {
@@ -174,6 +170,54 @@ export async function enrichNotifications(
174170
return enrichedNotifications;
175171
}
176172

173+
/**
174+
* Fetch notification details in batches to avoid overwhelming the API.
175+
*
176+
* @param notifications - The notifications to fetch details for.
177+
* @returns A map of notifications to their repository details.
178+
*/
179+
async function fetchNotificationDetailsInBatches(
180+
notifications: GitifyNotification[],
181+
): Promise<
182+
Map<GitifyNotification, FetchMergedDetailsTemplateQuery['repository']>
183+
> {
184+
const mergedResults: Map<
185+
GitifyNotification,
186+
FetchMergedDetailsTemplateQuery['repository']
187+
> = new Map();
188+
189+
const batchSize = Constants.GITHUB_API_MERGE_BATCH_SIZE;
190+
191+
for (
192+
let batchStart = 0;
193+
batchStart < notifications.length;
194+
batchStart += batchSize
195+
) {
196+
const batchIndex = Math.floor(batchStart / batchSize) + 1;
197+
const batchNotifications = notifications.slice(
198+
batchStart,
199+
batchStart + batchSize,
200+
);
201+
202+
try {
203+
const batchResults =
204+
await fetchNotificationDetailsForList(batchNotifications);
205+
206+
for (const [notification, repository] of batchResults) {
207+
mergedResults.set(notification, repository);
208+
}
209+
} catch (err) {
210+
rendererLogError(
211+
'fetchNotificationDetailsInBatches',
212+
`Failed to fetch merged notification details for batch ${batchIndex}`,
213+
err,
214+
);
215+
}
216+
}
217+
218+
return mergedResults;
219+
}
220+
177221
/**
178222
* Enrich a notification with additional details.
179223
*

0 commit comments

Comments
 (0)