From f8a98c4da26b55e26337f5d788a788d3e7986f9b Mon Sep 17 00:00:00 2001 From: haseeb5555 Date: Sat, 8 Feb 2025 19:51:28 +0500 Subject: [PATCH 1/4] fix:stargazers action with timeout and retry --- www/src/actions/stargazers.ts | 47 ++++++++++++++++++++++++----------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/www/src/actions/stargazers.ts b/www/src/actions/stargazers.ts index 6ac78a6..7df7aa1 100644 --- a/www/src/actions/stargazers.ts +++ b/www/src/actions/stargazers.ts @@ -16,7 +16,7 @@ export const fetchStargazers = async ({ GITHUB_TOKEN }: { GITHUB_TOKEN: string } throw new Error("GitHub token is required but was not provided. Set the GITHUB_TOKEN environment variable.") } - const makeRequest = async (url: string, useToken = true) => { + const makeRequest = async (url: string, useToken = true, retries = 3) => { const headers: Record = { Accept: "application/vnd.github+json", "X-GitHub-Api-Version": "2022-11-28", @@ -27,23 +27,42 @@ export const fetchStargazers = async ({ GITHUB_TOKEN }: { GITHUB_TOKEN: string } headers.Authorization = `Bearer ${GITHUB_TOKEN}` } - const res = await fetch(url, { headers }) - - if (res.status === 403) { - // rate limit reached, retry without token - if (useToken) { - return makeRequest(url, false) + try { + const controller = new AbortController() + const timeout = setTimeout(() => controller.abort(), 10000) + + const res = await fetch(url, { + headers, + signal: controller.signal + }) + clearTimeout(timeout) + + if (res.status === 403) { + if (useToken) { + return makeRequest(url, false, retries) + } + throw new Error("Rate limit reached for both authenticated and unauthenticated requests") } - throw new Error("Rate limit reached for both authenticated and unauthenticated requests") - } + if (!res.ok) { + if (retries > 0) { + await new Promise(resolve => setTimeout(resolve, 1000)) + return makeRequest(url, useToken, retries - 1) + } + const errorText = await res.text() + throw new Error(`GitHub API failed: ${res.status} ${res.statusText} - ${errorText}`) + } - if (!res.ok) { - const errorText = await res.text() - throw new Error(`GitHub API failed: ${res.status} ${res.statusText} - ${errorText}`) + return res + } catch (error) { + if (error instanceof Error && error.name === 'AbortError') { + if (retries > 0) { + return makeRequest(url, useToken, retries - 1) + } + throw new Error('Request timeout') + } + throw error } - - return res } try { From 871411f0ac0670de4aa96fa08e5dee17859dfd8b Mon Sep 17 00:00:00 2001 From: haseeb5555 Date: Sat, 8 Feb 2025 19:54:12 +0500 Subject: [PATCH 2/4] fix:web socket memory leak --- .../jstack/src/client/hooks/use-web-socket.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/jstack/src/client/hooks/use-web-socket.ts b/packages/jstack/src/client/hooks/use-web-socket.ts index dca0ee6..b3628c4 100644 --- a/packages/jstack/src/client/hooks/use-web-socket.ts +++ b/packages/jstack/src/client/hooks/use-web-socket.ts @@ -16,18 +16,19 @@ export function useWebSocket< eventsRef.current = events useEffect(() => { - if (opts?.enabled === false) { + if (opts?.enabled === false || !socket) { return } const defaultHandlers = { onConnect: () => {}, - onError: () => {}, + onError: (error: Error) => console.error("WebSocket error:", error), + onDisconnect: () => socket.reconnect(), } const mergedEvents = { ...defaultHandlers, - ...events, + ...eventsRef.current, // Use ref to avoid stale closures } const eventNames = Object.keys(mergedEvents) as Array< @@ -36,7 +37,6 @@ export function useWebSocket< eventNames.forEach((eventName) => { const handler = mergedEvents[eventName] - if (handler) { socket.on(eventName, handler) } @@ -45,8 +45,11 @@ export function useWebSocket< return () => { eventNames.forEach((eventName) => { const handler = mergedEvents[eventName] - socket.off(eventName, handler) + if (handler) { + socket.off(eventName, handler) + } }) + socket.close() } - }, [opts?.enabled]) + }, [socket, opts?.enabled]) // Add socket to dependencies } From eb16484b7a0f5770572208c801afd773d11e750c Mon Sep 17 00:00:00 2001 From: haseeb5555 Date: Sat, 8 Feb 2025 19:55:36 +0500 Subject: [PATCH 3/4] fix:table of content state --- packages/jstack/src/client/hooks/use-web-socket.ts | 2 +- www/src/ctx/use-table-of-contents.ts | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/jstack/src/client/hooks/use-web-socket.ts b/packages/jstack/src/client/hooks/use-web-socket.ts index b3628c4..6cd37fe 100644 --- a/packages/jstack/src/client/hooks/use-web-socket.ts +++ b/packages/jstack/src/client/hooks/use-web-socket.ts @@ -51,5 +51,5 @@ export function useWebSocket< }) socket.close() } - }, [socket, opts?.enabled]) // Add socket to dependencies + }, [socket, opts?.enabled]) } diff --git a/www/src/ctx/use-table-of-contents.ts b/www/src/ctx/use-table-of-contents.ts index b0fe708..7b35344 100644 --- a/www/src/ctx/use-table-of-contents.ts +++ b/www/src/ctx/use-table-of-contents.ts @@ -16,9 +16,8 @@ interface State { export const useTableOfContents = create()((set) => ({ allHeadings: [], activeHeadingIds: [], - setAllHeadings: (allHeadings) => set((state) => ({ allHeadings })), - sections: [], visibleSections: [], + setAllHeadings: (allHeadings) => set(() => ({ allHeadings })), setVisibleSections: (visibleSections) => - set((state) => (state.visibleSections.join() === visibleSections.join() ? {} : { visibleSections })), + set((state) => (state.visibleSections.join() === visibleSections.join() ? state : { ...state, visibleSections })), })) From 5cf4df46999e15d9a1f5bea60a8a40f6b45c360b Mon Sep 17 00:00:00 2001 From: haseeb5555 Date: Sat, 8 Feb 2025 20:01:39 +0500 Subject: [PATCH 4/4] feat(IO): enhance Redis pub/sub reliability and error handling --- packages/jstack/src/server/io.ts | 34 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/packages/jstack/src/server/io.ts b/packages/jstack/src/server/io.ts index 42c22ed..6e31e18 100644 --- a/packages/jstack/src/server/io.ts +++ b/packages/jstack/src/server/io.ts @@ -4,29 +4,41 @@ import { logger } from "jstack-shared" export class IO { private targetRoom: string | null = null private redis: Redis + private readonly timeout = 5000 // 5 seconds timeout constructor(redisUrl: string, redisToken: string) { this.redis = new Redis({ token: redisToken, url: redisUrl }) } - /** - * Sends to all connected clients (broadcast) - */ async emit(event: K, data: OutgoingEvents[K]) { - if (this.targetRoom) { - await this.redis.publish(this.targetRoom, [event, data]) + if (!this.targetRoom) { + throw new Error("No target room specified. Call .to(room) before .emit()") } - logger.success(`IO emitted to room "${this.targetRoom}":`, [event, data]) + try { + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error("Redis publish timeout")), this.timeout) + }) - // Reset target room after emitting - this.targetRoom = null + await Promise.race([ + this.redis.publish(this.targetRoom, [event, data]), + timeoutPromise + ]) + + logger.success(`IO emitted to room "${this.targetRoom}":`, [event, data]) + } catch (error) { + logger.error(`Failed to emit to room "${this.targetRoom}":`, error) + throw error + } finally { + // Reset target room after emitting + this.targetRoom = null + } } - /** - * Sends to all in a room - */ to(room: string): this { + if (!room) { + throw new Error("Room name cannot be empty") + } this.targetRoom = room return this }