From figma-pack
Implements circuit breakers and Node.js file-cached fallbacks for fault-tolerant Figma REST API integrations during outages and rate limits.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin figma-packThis skill is limited to using the following tools:
Production reliability patterns for Figma REST API integrations. Figma is an external dependency -- your application must handle its outages, rate limits, and slow responses without cascading failures.
Triages and mitigates Figma API incidents like outages, 403 auth failures, 429 rate limits using curl scripts, decision trees, and rotation steps.
Loads mandatory prerequisite context before every use_figma tool call for Figma writes or JS-executed reads like node edits, variable setup, component building, or file inspection.
Mandatory prerequisite skill for `use_figma` tool calls to execute JS in Figma files. Enables node create/edit/delete, variables/tokens setup, component building, auto-layout changes, property binding, and programmatic file inspection.
Share bugs, ideas, or general feedback.
Production reliability patterns for Figma REST API integrations. Figma is an external dependency -- your application must handle its outages, rate limits, and slow responses without cascading failures.
// Prevent cascading failures when Figma is down
class FigmaCircuitBreaker {
private failures = 0;
private lastFailure = 0;
private state: 'closed' | 'open' | 'half-open' = 'closed';
constructor(
private threshold = 5, // Open after 5 failures
private resetTimeMs = 30_000 // Try again after 30s
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailure > this.resetTimeMs) {
this.state = 'half-open';
console.log('[figma-circuit] State: half-open (testing recovery)');
} else {
throw new Error('Figma circuit breaker is OPEN -- failing fast');
}
}
try {
const result = await fn();
if (this.state === 'half-open') {
this.state = 'closed';
this.failures = 0;
console.log('[figma-circuit] State: closed (recovered)');
}
return result;
} catch (error) {
this.failures++;
this.lastFailure = Date.now();
if (this.failures >= this.threshold) {
this.state = 'open';
console.warn(`[figma-circuit] State: OPEN after ${this.failures} failures`);
}
throw error;
}
}
getState() { return this.state; }
}
const figmaBreaker = new FigmaCircuitBreaker();
// Usage
async function safeFigmaCall<T>(fn: () => Promise<T>): Promise<T> {
return figmaBreaker.execute(fn);
}
import { readFileSync, writeFileSync, existsSync } from 'fs';
// Serve cached data when Figma is unavailable
class FigmaFallbackCache {
constructor(private cacheDir = '.figma-cache') {}
private getPath(key: string) {
return `${this.cacheDir}/${key.replace(/[^a-zA-Z0-9]/g, '_')}.json`;
}
save(key: string, data: any) {
const { mkdirSync } = require('fs');
mkdirSync(this.cacheDir, { recursive: true });
writeFileSync(this.getPath(key), JSON.stringify({
data,
cachedAt: new Date().toISOString(),
}));
}
load(key: string): { data: any; cachedAt: string } | null {
const path = this.getPath(key);
if (!existsSync(path)) return null;
return JSON.parse(readFileSync(path, 'utf-8'));
}
}
const fallbackCache = new FigmaFallbackCache();
async function fetchWithFallback<T>(
cacheKey: string,
fetcher: () => Promise<T>
): Promise<{ data: T; fromCache: boolean; cachedAt?: string }> {
try {
const data = await safeFigmaCall(fetcher);
// Update cache with fresh data
fallbackCache.save(cacheKey, data);
return { data, fromCache: false };
} catch (error) {
console.warn(`Figma unavailable, loading cached ${cacheKey}`);
const cached = fallbackCache.load(cacheKey);
if (cached) {
return { data: cached.data as T, fromCache: true, cachedAt: cached.cachedAt };
}
throw new Error(`Figma unavailable and no cached data for ${cacheKey}`);
}
}
async function figmaRetry<T>(
fn: () => Promise<Response>,
maxRetries = 3
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fn();
if (res.ok) return res.json();
if (res.status === 429) {
const retryAfter = parseInt(res.headers.get('Retry-After') || '60');
if (attempt < maxRetries) {
console.warn(`429 -- waiting ${retryAfter}s (attempt ${attempt + 1}/${maxRetries})`);
await new Promise(r => setTimeout(r, retryAfter * 1000));
continue;
}
}
if (res.status >= 500 && attempt < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, attempt), 30_000);
const jitter = Math.random() * 1000;
await new Promise(r => setTimeout(r, delay + jitter));
continue;
}
throw new FigmaApiError(res.status, await res.text());
}
throw new Error('Max retries exceeded');
}
// Prevent requests from hanging indefinitely
async function figmaFetchWithTimeout(
path: string,
token: string,
timeoutMs = 15_000
): Promise<Response> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
try {
return await fetch(`https://api.figma.com${path}`, {
headers: { 'X-Figma-Token': token },
signal: controller.signal,
});
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw new Error(`Figma request timed out after ${timeoutMs}ms: ${path}`);
}
throw error;
} finally {
clearTimeout(timeout);
}
}
// Only make non-critical Figma calls when the API is healthy
class FigmaHealthTracker {
private healthy = true;
private lastCheck = 0;
private checkIntervalMs = 30_000;
async isHealthy(token: string): Promise<boolean> {
if (Date.now() - this.lastCheck < this.checkIntervalMs) {
return this.healthy;
}
try {
const res = await figmaFetchWithTimeout('/v1/me', token, 5000);
this.healthy = res.ok;
} catch {
this.healthy = false;
}
this.lastCheck = Date.now();
return this.healthy;
}
}
const healthTracker = new FigmaHealthTracker();
async function conditionalFigmaCall<T>(
token: string,
critical: boolean,
fn: () => Promise<T>,
fallback: () => Promise<T>
): Promise<T> {
const healthy = await healthTracker.isHealthy(token);
if (!healthy && !critical) {
console.log('Figma unhealthy, using fallback for non-critical call');
return fallback();
}
return fetchWithFallback('default', fn).then(r => r.data);
}
Retry-After header| Issue | Cause | Solution |
|---|---|---|
| Circuit stays open | Threshold too low | Increase threshold or decrease reset time |
| Stale fallback data | Cache not refreshed | Refresh cache on successful calls |
| Retry loops | Not respecting Retry-After | Always use the header value |
| Timeout too short | Large file responses | Increase timeout for /v1/files calls |
For policy enforcement, see figma-policy-guardrails.