-
-
Notifications
You must be signed in to change notification settings - Fork 42
feat: implement native IndexedDB cache-first data layer #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
6b5f054
9262534
af4c8f5
42136eb
ef9138f
53497b8
507504c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,60 @@ | ||||||||||
| import React, { useState } from "react" | ||||||||||
| import { tokenService, githubService } from "../services" | ||||||||||
| // funtion to accept PAT and organisation name | ||||||||||
| export default function TestServices() { | ||||||||||
| const [tokenInput, setTokenInput] = useState("") | ||||||||||
| const [orgInput, setOrgInput] = useState("") | ||||||||||
|
|
||||||||||
| const testFlow = async () => { | ||||||||||
| if (!tokenInput) { | ||||||||||
| alert("Please enter GitHub token") | ||||||||||
| return | ||||||||||
| } | ||||||||||
|
|
||||||||||
| if (!orgInput) { | ||||||||||
| alert("Please enter organization name") | ||||||||||
| return | ||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||
| } | ||||||||||
|
|
||||||||||
| tokenService.setToken(tokenInput) | ||||||||||
| // funtion which fetches org repos is called | ||||||||||
| try { | ||||||||||
| const repos = await githubService.fetchOrgReposWithCache( | ||||||||||
| orgInput, | ||||||||||
| tokenInput | ||||||||||
| ) | ||||||||||
|
|
||||||||||
| console.log("Repos count:", repos.length) | ||||||||||
| console.log("Repo names:") | ||||||||||
| repos.forEach(repo => console.log(repo.name)) | ||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: # First, verify the pattern exists in the file
rg -nP 'forEach\s*\(\s*\w+\s*=>\s*console\.log\(' src/components/TestServices.tsxRepository: AOSSIE-Org/OrgExplorer Length of output: 120 🏁 Script executed: # Also check the full context around line 33
sed -n '25,40p' src/components/TestServices.tsxRepository: AOSSIE-Org/OrgExplorer Length of output: 447 🏁 Script executed: # Verify the file exists and check its basic properties
ls -la src/components/TestServices.tsx && head -35 src/components/TestServices.tsx | tail -10Repository: AOSSIE-Org/OrgExplorer Length of output: 416 Fix Line 33 uses an expression-bodied callback in Proposed fix- repos.forEach(repo => console.log(repo.name))
+ repos.forEach((repo) => {
+ console.log(repo.name)
+ })📝 Committable suggestion
Suggested change
🧰 Tools🪛 Biome (2.4.4)[error] 33-33: This callback passed to forEach() iterable method should not return a value. (lint/suspicious/useIterableCallbackReturn) 🤖 Prompt for AI Agents |
||||||||||
|
|
||||||||||
| } catch (error) { | ||||||||||
| console.error("Error:", error) | ||||||||||
| alert("Failed to fetch repositories"); | ||||||||||
|
|
||||||||||
| } | ||||||||||
| } | ||||||||||
| // input fileds to enter PAT and org name | ||||||||||
| return ( | ||||||||||
| <div style={{ padding: "1rem" }}> | ||||||||||
| <input | ||||||||||
| type="password" | ||||||||||
| placeholder="Enter GitHub PAT" | ||||||||||
| value={tokenInput} | ||||||||||
| onChange={(e) => setTokenInput(e.target.value)} | ||||||||||
| /> | ||||||||||
|
|
||||||||||
| <input | ||||||||||
| type="text" | ||||||||||
| placeholder="Enter organization name" | ||||||||||
| value={orgInput} | ||||||||||
| onChange={(e) => setOrgInput(e.target.value)} | ||||||||||
| style={{ marginLeft: "10px" }} | ||||||||||
| /> | ||||||||||
|
|
||||||||||
| <button onClick={testFlow} style={{ marginLeft: "10px" }}> | ||||||||||
|
swathi2006 marked this conversation as resolved.
Outdated
|
||||||||||
| Test Services | ||||||||||
|
swathi2006 marked this conversation as resolved.
Outdated
|
||||||||||
| </button> | ||||||||||
| </div> | ||||||||||
| ) | ||||||||||
| } | ||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,91 @@ | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // export default cacheService; | ||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||
| import { saveToIDB, getFromIDB } from "./idbService" | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Minimal GitHub Repo Type | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| export interface GitHubRepo { | ||||||||||||||||||||||||||||||||
| id: number | ||||||||||||||||||||||||||||||||
| name: string | ||||||||||||||||||||||||||||||||
| stargazers_count: number | ||||||||||||||||||||||||||||||||
| forks_count: number | ||||||||||||||||||||||||||||||||
| language: string | null | ||||||||||||||||||||||||||||||||
| updated_at: string | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Structured cache entry format | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| type RepoCacheEntry = { | ||||||||||||||||||||||||||||||||
| data: GitHubRepo[] | ||||||||||||||||||||||||||||||||
| savedAt: number | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||
| * Cache expiry time (10 minutes) | ||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||
| const MAX_CACHE_AGE = 1000 * 60 * 10 | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const cacheService = { | ||||||||||||||||||||||||||||||||
| // repos of an org are saved in cache | ||||||||||||||||||||||||||||||||
| async saveRepos(org: string, data: GitHubRepo[] | RepoCacheEntry): Promise<void> { | ||||||||||||||||||||||||||||||||
| const entry: RepoCacheEntry = Array.isArray(data) | ||||||||||||||||||||||||||||||||
| ? { data, savedAt: Date.now() } | ||||||||||||||||||||||||||||||||
| : data | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| console.log(`Saving ${org} repos to IDB`) | ||||||||||||||||||||||||||||||||
| await saveToIDB(org, entry) | ||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||
| // repos are fetched from cache if they are in cache already | ||||||||||||||||||||||||||||||||
| async getRepos(org: string): Promise<GitHubRepo[] | null> { | ||||||||||||||||||||||||||||||||
| const entry = await getFromIDB(org) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
|
Comment on lines
+41
to
+43
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Treat cache read failures as cache misses to preserve API fallback. If IndexedDB read throws, this currently propagates and prevents Proposed fix async getRepos(org: string): Promise<GitHubRepo[] | null> {
- const entry = await getFromIDB(org)
+ let entry: unknown
+ try {
+ entry = await getFromIDB(org)
+ } catch (error) {
+ console.warn(`Cache read failed for ${org}:`, error)
+ return null
+ }
if (!entry) {
console.log("No cache found")
return null📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
| if (!entry) { | ||||||||||||||||||||||||||||||||
| console.log("No cache found") | ||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| console.log("Cache found") | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Handle old format (raw array) | ||||||||||||||||||||||||||||||||
| if (Array.isArray(entry)) { | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| console.log("Detected old cache format, migrating..."); | ||||||||||||||||||||||||||||||||
| const migratedEntry: RepoCacheEntry = { | ||||||||||||||||||||||||||||||||
| data: entry, | ||||||||||||||||||||||||||||||||
| savedAt: Date.now() | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| cacheService.saveRepos(org, migratedEntry).catch(err => | ||||||||||||||||||||||||||||||||
| console.error("Migration failed:", err) | ||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return entry | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // Handle structured format | ||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||
| typeof entry === "object" && | ||||||||||||||||||||||||||||||||
| entry !== null && | ||||||||||||||||||||||||||||||||
| "data" in entry && | ||||||||||||||||||||||||||||||||
| "savedAt" in entry | ||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||
| const typedEntry = entry as RepoCacheEntry | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| // TTL CHECK | ||||||||||||||||||||||||||||||||
| if (Date.now() - typedEntry.savedAt > MAX_CACHE_AGE) { | ||||||||||||||||||||||||||||||||
| console.log("Cache expired") | ||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| return typedEntry.data | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Harden runtime validation for structured cache entries. Checking only key presence allows malformed values through (e.g., Proposed fix- if (
- typeof entry === "object" &&
- entry !== null &&
- "data" in entry &&
- "savedAt" in entry
- ) {
+ if (
+ typeof entry === "object" &&
+ entry !== null &&
+ "data" in entry &&
+ Array.isArray((entry as { data: unknown }).data) &&
+ "savedAt" in entry &&
+ typeof (entry as { savedAt: unknown }).savedAt === "number"
+ ) {
const typedEntry = entry as RepoCacheEntry🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| console.warn(`Cache for ${org} is in an unrecognized format.`) | ||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| export default cacheService | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,77 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import cacheService from "./cacheService"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import type { GitHubRepo } from "./cacheService" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const GITHUB_API_URL = 'https://api.github.com'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const githubService = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async fetchOrgRepos(org: string, token: string): Promise<GitHubRepo[]> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const url = `${GITHUB_API_URL}/orgs/${org}/repos`; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Fetching from:", url); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response = await fetch(url, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'GET', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| headers: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: `Bearer ${token}`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Accept: 'application/vnd.github+json', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!response.ok) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.status === 401) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Invalid or expired GitHub token. Please check your PAT."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.status === 403) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Rate limit exceeded. Please try again later."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.status === 404) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error("Organization not found."); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `GitHub API error: ${response.status} ${response.statusText}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return await response.json(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+10
to
+39
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fetch all repository pages instead of only the first page. GitHub org repos API is paginated; the current implementation can silently cache only the first 30 repos for larger organizations. Proposed fix- const url = `${GITHUB_API_URL}/orgs/${org}/repos`;
- console.log("Fetching from:", url);
-
- const response = await fetch(url, {
- method: 'GET',
- headers: {
- Authorization: `Bearer ${token}`,
- Accept: 'application/vnd.github+json',
- }
- });
+ const repos: GitHubRepo[] = [];
+ let page = 1;
+
+ while (true) {
+ const url = `${GITHUB_API_URL}/orgs/${encodeURIComponent(org)}/repos?per_page=100&page=${page}`;
+ console.log("Fetching from:", url);
+
+ const response = await fetch(url, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${token}`,
+ Accept: "application/vnd.github+json",
+ },
+ });
- if (!response.ok) {
- if (response.status === 401) {
- throw new Error("Invalid or expired GitHub token. Please check your PAT.");
- }
+ if (!response.ok) {
+ if (response.status === 401) {
+ throw new Error("Invalid or expired GitHub token. Please check your PAT.");
+ }
+ if (response.status === 403) {
+ throw new Error("Rate limit exceeded. Please try again later.");
+ }
+ if (response.status === 404) {
+ throw new Error("Organization not found.");
+ }
+ throw new Error(`GitHub API error: ${response.status} ${response.statusText}`);
+ }
- if (response.status === 403) {
- throw new Error("Rate limit exceeded. Please try again later.");
- }
+ const batch = (await response.json()) as GitHubRepo[];
+ repos.push(...batch);
- if (response.status === 404) {
- throw new Error("Organization not found.");
- }
+ if (batch.length < 100) break;
+ page += 1;
+ }
- throw new Error(
- `GitHub API error: ${response.status} ${response.statusText}`
- );
-}
-
- return await response.json();
+ return repos;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.error(`Error fetching repositories for organization ${org}:`, error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error instanceof Error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert(error.message) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert("Something went wrong") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw error; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async fetchOrgReposWithCache(org: string, token: string): Promise<GitHubRepo[]> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 1: Check IDB cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const cachedRepos = await cacheService.getRepos(org); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (cachedRepos) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log("Using cached repos"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return cachedRepos; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 2: Fetch from GitHub | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const repos = await this.fetchOrgRepos(org, token); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Step 3: Save structured cache | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| await cacheService.saveRepos(org, { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data: repos, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| savedAt: Date.now() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return repos; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default githubService; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| // Name of the IndexedDB database | ||
| const DB_NAME = "OrgExplorerDB"; | ||
|
|
||
| // Name of the object store (similar to a table in SQL) | ||
| const STORE_NAME = "repos"; | ||
|
|
||
| /** | ||
| * Opens (or creates) the IndexedDB database. | ||
| * Returns a Promise that resolves with the database instance. | ||
| */ | ||
| function openDB(): Promise<IDBDatabase> { | ||
| return new Promise((resolve, reject) => { | ||
| // Open database with version 1 | ||
| const request = indexedDB.open(DB_NAME, 1); | ||
|
|
||
| /** | ||
| * This event runs only when: | ||
| * - Database is created for the first time | ||
| * - Version number is increased | ||
| */ | ||
| request.onupgradeneeded = () => { | ||
| const db = request.result; | ||
|
|
||
| // Create object store if it does not already exist | ||
| if (!db.objectStoreNames.contains(STORE_NAME)) { | ||
| db.createObjectStore(STORE_NAME); | ||
| } | ||
| }; | ||
|
|
||
| // If database opens successfully, resolve the Promise | ||
| request.onsuccess = () => resolve(request.result); | ||
|
|
||
| // If an error occurs while opening, reject the Promise | ||
| request.onerror = () => reject(request.error); | ||
| }); | ||
| } | ||
|
|
||
| /** | ||
| * Saves data into IndexedDB. | ||
| * @param key Unique identifier (e.g., organization name) | ||
| * @param value Data to store (e.g., repos array with metadata) | ||
| */ | ||
| export async function saveToIDB(key: string, value: any) { | ||
| const db = await openDB(); | ||
|
|
||
| // Create a read-write transaction | ||
| const tx = db.transaction(STORE_NAME, "readwrite"); | ||
|
|
||
| // Access the object store | ||
| const store = tx.objectStore(STORE_NAME); | ||
|
|
||
| // Insert or update value using the provided key | ||
| store.put(value, key); | ||
| } | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
|
|
||
| /** | ||
| * Retrieves data from IndexedDB using a key. | ||
| * @param key Unique identifier (e.g., organization name) | ||
| * @returns Stored value or null if not found | ||
| */ | ||
| export async function getFromIDB(key: string): Promise<any | null> { | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| const db = await openDB(); | ||
|
|
||
| // Create a read-only transaction | ||
| const tx = db.transaction(STORE_NAME, "readonly"); | ||
|
|
||
| // Access the object store | ||
| const store = tx.objectStore(STORE_NAME); | ||
|
|
||
| return new Promise((resolve) => { | ||
| const req = store.get(key); | ||
|
|
||
| // Resolve with stored result or null if not found | ||
| req.onsuccess = () => resolve(req.result || null); | ||
| }); | ||
|
coderabbitai[bot] marked this conversation as resolved.
Outdated
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| import tokenService from "./tokenService"; | ||
| import githubService from "./githubService"; | ||
| import cacheService from "./cacheService"; | ||
|
|
||
| const { fetchOrgRepos } = githubService; | ||
| const { saveRepos, getRepos } = cacheService; | ||
|
|
||
| export { | ||
| tokenService, | ||
| githubService, | ||
| cacheService, | ||
| fetchOrgRepos, | ||
| saveRepos, | ||
| getRepos | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| /** | ||
| * Service for managing the GitHub Personal Access Token (PAT). | ||
| * Currently stores the token in memory for security and as a temporary measure. | ||
| * | ||
| * | ||
| */ | ||
|
|
||
| let _token: string | null = null; | ||
|
|
||
| const tokenService = { | ||
| /** | ||
| * Sets the GitHub PAT in memory. | ||
| * @param token | ||
| */ | ||
| setToken(token: string): void { | ||
| _token = token; | ||
| }, | ||
|
|
||
| /** | ||
| * Gets the GitHub PAT from memory. | ||
| * @returns | ||
| */ | ||
| getToken(): string | null { | ||
| return _token; | ||
| }, | ||
|
|
||
| /** | ||
| * Removes the GitHub PAT from memory. | ||
| */ | ||
| removeToken(): void { | ||
| _token = null; | ||
| } | ||
| }; | ||
|
|
||
| export default tokenService; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧹 Nitpick | 🔵 Trivial
Unify token flow: remove redundant tokenService write or consume it in githubService.
tokenService.setToken(trimmedToken)is currently unused bygithubService.fetchOrgReposWithCache(trimmedOrg, trimmedToken)(token is passed directly), which creates two parallel auth paths and weakens API clarity.♻️ Minimal cleanup option
Also applies to: 23-29
🤖 Prompt for AI Agents