-
Notifications
You must be signed in to change notification settings - Fork 0
feat: add resilience layer with caching and graceful degradation for … #2
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,3 @@ | ||
| SUPABASE_URL=https://cuwxnggcavvzbqbidaml.supabase.co | ||
| SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImN1d3huZ2djYXZ2emJxYmlkYW1sIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NjMzMTA5NzAsImV4cCI6MjA3ODg4Njk3MH0.X0BjHpf6hR3v3Iwv7BN5wkJhewLTzCZnbTq090vuGi0 | ||
| SIMULATE_SUPABASE_DOWN=false | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -23,3 +23,4 @@ yarn-error.log* | |
| /src/data/readmeSections.json | ||
| /.vscode/ | ||
|
|
||
| /src/data/cachedSignatories.json | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| #!/usr/bin/env node | ||
| // Load .env variables | ||
| require('dotenv').config(); | ||
|
|
||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| const outFile = path.resolve(__dirname, '..', 'src', 'data', 'cachedSignatories.json'); | ||
|
|
||
| async function fetchSignatories() { | ||
| // Try to load Supabase credentials from environment | ||
| const supabaseUrl = process.env.SUPABASE_URL; | ||
| const supabaseAnonKey = process.env.SUPABASE_ANON_KEY; | ||
|
|
||
| if (!supabaseUrl || !supabaseAnonKey) { | ||
| console.warn('⚠️ SUPABASE_URL or SUPABASE_ANON_KEY not found in environment'); | ||
| console.warn('⚠️ Skipping signatories cache generation'); | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| console.log('🔄 Fetching signatories from Supabase...'); | ||
|
|
||
| // Use fetch API to query Supabase REST API directly | ||
| const response = await fetch( | ||
|
||
| `${supabaseUrl}/rest/v1/signatures?select=id,name,avatar_url,profile_url,created_at,privacy_level,auth_provider,name_only_location&order=created_at.desc&limit=100`, | ||
| { | ||
| headers: { | ||
| apikey: supabaseAnonKey, | ||
| Authorization: `Bearer ${supabaseAnonKey}`, | ||
| }, | ||
| } | ||
| ); | ||
|
|
||
| if (!response.ok) { | ||
| throw new Error(`HTTP ${response.status}: ${response.statusText}`); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| console.log(`✅ Successfully fetched ${data.length} signatories`); | ||
| return data; | ||
| } catch (error) { | ||
| console.error('❌ Failed to fetch signatories from Supabase:', error.message); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| async function run() { | ||
| const signatories = await fetchSignatories(); | ||
|
|
||
| // Create output directory | ||
| fs.mkdirSync(path.dirname(outFile), { recursive: true }); | ||
|
|
||
| if (signatories) { | ||
| // Write successful cache with metadata | ||
| const cacheData = { | ||
| cached_at: new Date().toISOString(), | ||
| count: signatories.length, | ||
| signatories: signatories, | ||
| }; | ||
| fs.writeFileSync(outFile, JSON.stringify(cacheData, null, 2), 'utf8'); | ||
| console.log(`✅ Cached ${signatories.length} signatories to ${outFile}`); | ||
| } else { | ||
| // Write empty cache with error flag | ||
| const cacheData = { | ||
| cached_at: new Date().toISOString(), | ||
| count: 0, | ||
| error: 'Failed to fetch from Supabase', | ||
| signatories: [], | ||
| }; | ||
| fs.writeFileSync(outFile, JSON.stringify(cacheData, null, 2), 'utf8'); | ||
| console.log('⚠️ Created empty cache file'); | ||
| } | ||
| } | ||
|
|
||
| run().catch((err) => { | ||
| console.error('❌ Cache script failed:', err); | ||
| process.exit(1); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,9 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useEffect, useState } from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import { useSupabase } from '@site/src/utils/supabase'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import styles from './SignersList.module.css'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import mockSignatures from '@site/src/data/mockSignatures.json'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| import cachedSignatories from '@site/src/data/cachedSignatories.json'; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| interface Signer { | ||||||||||||||||||||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -22,15 +24,25 @@ const USE_MOCK_DATA = process.env.NODE_ENV === 'development'; | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| interface SignersListProps { | ||||||||||||||||||||||||||||||||||||||||||||||||
| variant?: 'compact' | 'full'; | ||||||||||||||||||||||||||||||||||||||||||||||||
| showErrorBanner?: boolean; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| export default function SignersList({ variant = 'compact' }: SignersListProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| export default function SignersList({ variant = 'compact', showErrorBanner = false }: SignersListProps) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| const { siteConfig } = useDocusaurusContext(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const supabase = useSupabase(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [signers, setSigners] = useState<Signer[]>(() => (USE_MOCK_DATA ? (mockSignatures as Signer[]) : [])); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(() => !USE_MOCK_DATA && !!supabase); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Check if we should simulate Supabase being down (for testing) | ||||||||||||||||||||||||||||||||||||||||||||||||
| const simulateDown = Boolean(siteConfig.customFields?.simulateSupabaseDown); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const shouldUseMockData = USE_MOCK_DATA && !simulateDown; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const [signers, setSigners] = useState<Signer[]>(() => (shouldUseMockData ? (mockSignatures as Signer[]) : [])); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(() => !shouldUseMockData && !!supabase); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const [usingCache, setUsingCache] = useState(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (USE_MOCK_DATA) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Query parameter takes precedence over USE_MOCK_DATA | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (shouldUseMockData) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Loading mock signatures:', mockSignatures.length, 'entries'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -41,26 +53,52 @@ export default function SignersList({ variant = 'compact' }: SignersListProps) { | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const load = async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| const { data, error } = await supabase | ||||||||||||||||||||||||||||||||||||||||||||||||
| .from('signatures') | ||||||||||||||||||||||||||||||||||||||||||||||||
| .select( | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'id, name, avatar_url, profile_url, created_at, privacy_level, auth_provider, name_only_location' | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .order('created_at', { ascending: false }) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .limit(100); | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to load signatures', error); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setError(null); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUsingCache(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||
| // Simulate Supabase being down for testing | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (simulateDown) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.warn('🧪 [TEST] Simulating Supabase outage (set SIMULATE_SUPABASE_DOWN=false in .env to disable)'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw new Error('Simulated Supabase outage'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const { data, error: supabaseError } = await supabase | ||||||||||||||||||||||||||||||||||||||||||||||||
| .from('signatures') | ||||||||||||||||||||||||||||||||||||||||||||||||
| .select( | ||||||||||||||||||||||||||||||||||||||||||||||||
| 'id, name, avatar_url, profile_url, created_at, privacy_level, auth_provider, name_only_location' | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .order('created_at', { ascending: false }) | ||||||||||||||||||||||||||||||||||||||||||||||||
| .limit(100); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (supabaseError) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| throw supabaseError; | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| setSigners(data || []); | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Loaded', data?.length || 0, 'signatures from Supabase'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (err) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('Failed to load signatures from Supabase:', err); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setError('Connection to Supabase failed'); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| // Fallback to cached data | ||||||||||||||||||||||||||||||||||||||||||||||||
| if (cachedSignatories && cachedSignatories.signatories) { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.log('Falling back to cached data:', cachedSignatories.signatories.length, 'entries'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setSigners(cachedSignatories.signatories as Signer[]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| setUsingCache(true); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||
| console.error('No cached data available'); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||
| setLoading(false); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||
| load(); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| const handler = () => load(); | ||||||||||||||||||||||||||||||||||||||||||||||||
| window.addEventListener('signature-changed', handler); | ||||||||||||||||||||||||||||||||||||||||||||||||
| return () => window.removeEventListener('signature-changed', handler); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [supabase]); | ||||||||||||||||||||||||||||||||||||||||||||||||
| }, [supabase, simulateDown]); | ||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| if (loading) return <div>Loading signers…</div>; | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -75,6 +113,17 @@ export default function SignersList({ variant = 'compact' }: SignersListProps) { | |||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <> | ||||||||||||||||||||||||||||||||||||||||||||||||
| {showErrorBanner && error && usingCache && ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <div className={styles.errorBanner}> | ||||||||||||||||||||||||||||||||||||||||||||||||
| {error}. Showing cached signatories | ||||||||||||||||||||||||||||||||||||||||||||||||
| {cachedSignatories.cached_at && ( | ||||||||||||||||||||||||||||||||||||||||||||||||
| <span className={styles.cacheDate}> | ||||||||||||||||||||||||||||||||||||||||||||||||
| {' '} | ||||||||||||||||||||||||||||||||||||||||||||||||
| (as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')}) | ||||||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+116
to
+123
|
||||||||||||||||||||||||||||||||||||||||||||||||
| {showErrorBanner && error && usingCache && ( | |
| <div className={styles.errorBanner}> | |
| {error}. Showing cached signatories | |
| {cachedSignatories.cached_at && ( | |
| <span className={styles.cacheDate}> | |
| {' '} | |
| (as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')}) | |
| </span> | |
| {showErrorBanner && error && ( | |
| <div className={styles.errorBanner}> | |
| {error}. | |
| {usingCache ? ( | |
| <> | |
| {' '}Showing cached signatories | |
| {cachedSignatories.cached_at && ( | |
| <span className={styles.cacheDate}> | |
| {' '} | |
| (as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')}) | |
| </span> | |
| )} | |
| </> | |
| ) : ( | |
| <> No cached data available.</> |
Copilot
AI
Dec 5, 2025
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.
Missing error handling if cachedSignatories.cached_at contains an invalid date string. Consider wrapping the new Date() call in a try-catch or validating the date before rendering to prevent potential runtime errors.
| {cachedSignatories.cached_at && ( | |
| <span className={styles.cacheDate}> | |
| {' '} | |
| (as of {new Date(cachedSignatories.cached_at).toLocaleString('en-US')}) | |
| </span> | |
| )} | |
| {cachedSignatories.cached_at && (() => { | |
| const cacheDate = new Date(cachedSignatories.cached_at); | |
| if (!isNaN(cacheDate.getTime())) { | |
| return ( | |
| <span className={styles.cacheDate}> | |
| {' '} | |
| (as of {cacheDate.toLocaleString('en-US')}) | |
| </span> | |
| ); | |
| } | |
| return ( | |
| <span className={styles.cacheDate}> | |
| {' '} | |
| (as of unknown date) | |
| </span> | |
| ); | |
| })()} |
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.
The
.envfile should not be committed to version control as it contains sensitive credentials. Add.envto.gitignoreand create a.env.examplefile with placeholder values to document required environment variables.