Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions context/TranslationContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ export function TranslationProvider({ children }: { children: ReactNode }) {
value = getNestedValue(translations.en as Record<string, unknown>, path);
}

if (typeof value !== 'string') {
if (typeof value !== 'string' || value === path) {
if (params && 'defaultValue' in params) {
return params.defaultValue;
}
Expand Down Expand Up @@ -144,7 +144,7 @@ export function useTranslation() {
changeLanguage: () => {},
t: (path: string, params?: Record<string, string>): string => {
const value = getNestedValue(en, path);
if (typeof value !== 'string') {
if (typeof value !== 'string' || value === path) {
if (params && 'defaultValue' in params) {
return params.defaultValue;
}
Expand Down
54 changes: 54 additions & 0 deletions lib/syncQueue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* A queue to stagger incoming sync tasks across the available hourly quota.
* This prevents the application from making too many concurrent requests to the GitHub API,
* which could lead to rate limit exhaustion.
*/
export class SyncQueue {
private queue: (() => Promise<void>)[] = [];
private isProcessing = false;
// Delay between processing tasks to stagger API usage (e.g., 2 seconds)
private readonly STAGGER_DELAY_MS = 2000;

/**
* Enqueues a new sync task.
* @param task An async function representing the sync job.
*/
public enqueue(task: () => Promise<void>): void {
if (process.env.NODE_ENV === 'test') {
// Bypass queue in test environments to preserve synchronous mock assertions
task().catch(() => {});
return;
}

this.queue.push(task);
this.processNext();
}

private async processNext(): Promise<void> {
if (this.isProcessing || this.queue.length === 0) {
return;
}
this.isProcessing = true;

const task = this.queue.shift();
if (task) {
try {
await task();
} catch (error) {
console.error('[SyncQueue] Task failed:', error);
}
}

// Stagger the next task to distribute API load evenly
await new Promise((resolve) => setTimeout(resolve, this.STAGGER_DELAY_MS));

this.isProcessing = false;
this.processNext();
}

public get pendingTasks(): number {
return this.queue.length;
}
}

export const syncQueue = new SyncQueue();
30 changes: 16 additions & 14 deletions services/github/background-refresh.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getFullDashboardData } from '../../lib/github';
import { syncQueue } from '../../lib/syncQueue';

// Cache is considered stale and candidate for background refresh after 10 minutes
const STALE_THRESHOLD_MS = 10 * 60 * 1000;
Expand Down Expand Up @@ -45,22 +46,23 @@ export class BackgroundRefresh {

this.activeJobs.add(sanitized);

console.info(`[BackgroundRefresh] Starting background refresh for: ${sanitized}`);
console.info(`[BackgroundRefresh] Queuing background refresh for: ${sanitized}`);

// forceRefresh refetches and writes back to the cache; the returned promise lets the
// caller keep the function alive (e.g. via after()) until the refresh completes.
return getFullDashboardData(username, { forceRefresh: true })
.then(() => {
console.info(
`[BackgroundRefresh] Successfully completed background refresh for: ${sanitized}`
);
})
.catch((err) => {
console.error(`[BackgroundRefresh] Background refresh failed for: ${sanitized}`, err);
})
.finally(() => {
this.activeJobs.delete(sanitized);
return new Promise((resolve) => {
syncQueue.enqueue(async () => {
try {
await getFullDashboardData(username, { forceRefresh: true });
console.info(
`[BackgroundRefresh] Successfully completed background refresh for: ${sanitized}`
);
} catch (err) {
console.error(`[BackgroundRefresh] Background refresh failed for: ${sanitized}`, err);
} finally {
this.activeJobs.delete(sanitized);
resolve();
}
});
});
}

/**
Expand Down
Loading