From miro-pack
Optimizes Miro REST API v2 performance with cursor pagination, LRU caching, request batching, and connection pooling for high-throughput integrations.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin miro-packThis skill is limited to using the following tools:
Optimize Miro REST API v2 throughput and latency. Key levers: minimize API calls with cursor pagination, cache board/item data, batch writes with controlled concurrency, and use connection pooling.
Implements Miro REST API v2 rate limiting with credit tracking from headers, exponential backoff with jitter, and request queuing for 429 handling.
Automates Miro whiteboard tasks: create/manage boards, sticky notes, frames, items, sharing, connectors via Rube MCP (Composio). Requires active Miro connection and tool schema search.
Automates Miro whiteboard operations like listing/creating boards, sticky notes, frames, sharing, connectors via Composio's Rube MCP toolkit. Use for programmatic Miro workflows.
Share bugs, ideas, or general feedback.
Optimize Miro REST API v2 throughput and latency. Key levers: minimize API calls with cursor pagination, cache board/item data, batch writes with controlled concurrency, and use connection pooling.
Typical latencies for api.miro.com (US region):
| Operation | Endpoint | P50 | P95 | Credits |
|---|---|---|---|---|
| Get board | GET /v2/boards/{id} | 80ms | 200ms | Level 1 |
| List items (50) | GET /v2/boards/{id}/items?limit=50 | 120ms | 350ms | Level 1 |
| Create sticky note | POST /v2/boards/{id}/sticky_notes | 150ms | 400ms | Level 2 |
| Create connector | POST /v2/boards/{id}/connectors | 160ms | 420ms | Level 2 |
| Update item | PATCH /v2/boards/{id}/items/{id} | 130ms | 350ms | Level 2 |
| Delete item | DELETE /v2/boards/{id}/items/{id} | 100ms | 280ms | Level 2 |
Miro v2 uses cursor-based pagination. Fetch only what you need.
// Efficient paginated iterator
async function* paginateItems(
boardId: string,
options: { type?: string; limit?: number } = {}
): AsyncGenerator<MiroBoardItem> {
const limit = options.limit ?? 50; // Max 50 per page
let cursor: string | undefined;
do {
const params = new URLSearchParams({ limit: String(limit) });
if (options.type) params.set('type', options.type);
if (cursor) params.set('cursor', cursor);
const response = await fetch(
`https://api.miro.com/v2/boards/${boardId}/items?${params}`,
{ headers: { 'Authorization': `Bearer ${token}` } }
);
const page = await response.json();
for (const item of page.data) {
yield item;
}
cursor = page.cursor; // undefined when no more pages
} while (cursor);
}
// Usage: process items without loading entire board into memory
for await (const item of paginateItems(boardId, { type: 'sticky_note' })) {
await processItem(item);
}
import { LRUCache } from 'lru-cache';
const boardCache = new LRUCache<string, unknown>({
max: 500, // Max 500 cached entries
ttl: 60_000, // 1 minute TTL
updateAgeOnGet: true, // Extend TTL on access
updateAgeOnHas: false,
});
async function getCachedBoard(boardId: string): Promise<MiroBoard> {
const cacheKey = `board:${boardId}`;
const cached = boardCache.get(cacheKey);
if (cached) return cached as MiroBoard;
const board = await miroFetch(`/v2/boards/${boardId}`);
boardCache.set(cacheKey, board);
return board;
}
// Invalidate on webhook events
function onBoardEvent(event: MiroBoardEvent) {
boardCache.delete(`board:${event.boardId}`);
boardCache.delete(`items:${event.boardId}`);
}
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function getCachedItems(boardId: string): Promise<MiroBoardItem[]> {
const key = `miro:items:${boardId}`;
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
// Fetch all items
const items: MiroBoardItem[] = [];
for await (const item of paginateItems(boardId)) {
items.push(item);
}
// Cache for 2 minutes (boards with active editing should use shorter TTL)
await redis.setex(key, 120, JSON.stringify(items));
return items;
}
// Webhook-driven cache invalidation
async function invalidateOnEvent(event: MiroBoardEvent) {
const keys = [
`miro:items:${event.boardId}`,
`miro:board:${event.boardId}`,
];
await redis.del(...keys);
}
import PQueue from 'p-queue';
// Create items in parallel with controlled concurrency
async function bulkCreateStickyNotes(
boardId: string,
notes: Array<{ content: string; color: string; x: number; y: number }>
): Promise<string[]> {
const queue = new PQueue({
concurrency: 5, // Max 5 parallel requests
interval: 1000, // Per-second window
intervalCap: 10, // Max 10 requests per second
});
const ids: string[] = [];
for (const note of notes) {
queue.add(async () => {
const result = await miroFetch(`/v2/boards/${boardId}/sticky_notes`, 'POST', {
data: { content: note.content, shape: 'square' },
style: { fillColor: note.color },
position: { x: note.x, y: note.y },
});
ids.push(result.id);
});
}
await queue.onIdle(); // Wait for all to complete
return ids;
}
// Example: Create a grid of 50 sticky notes efficiently
const notes = Array.from({ length: 50 }, (_, i) => ({
content: `Note ${i + 1}`,
color: 'light_yellow',
x: (i % 10) * 250,
y: Math.floor(i / 10) * 250,
}));
const createdIds = await bulkCreateStickyNotes(boardId, notes);
console.log(`Created ${createdIds.length} sticky notes`);
import { Agent } from 'https';
// Reuse TCP connections across requests
const httpsAgent = new Agent({
keepAlive: true,
maxSockets: 10, // Max 10 concurrent connections to api.miro.com
maxFreeSockets: 5, // Keep 5 idle connections in pool
timeout: 30000, // Socket timeout
});
async function miroFetchPooled(path: string, options: RequestInit = {}) {
// Note: Node.js native fetch doesn't support custom agents directly.
// Use undici or node-fetch for connection pooling.
const { default: fetch } = await import('node-fetch');
return fetch(`https://api.miro.com${path}`, {
...options,
agent: httpsAgent,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
...options.headers,
},
});
}
async function instrumentedMiroFetch<T>(
operation: string,
path: string,
method = 'GET',
body?: unknown,
): Promise<{ data: T; metrics: RequestMetrics }> {
const start = performance.now();
const response = await fetch(`https://api.miro.com${path}`, {
method,
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
...(body ? { body: JSON.stringify(body) } : {}),
});
const duration = performance.now() - start;
const metrics: RequestMetrics = {
operation,
path,
method,
status: response.status,
durationMs: Math.round(duration),
rateLimitRemaining: parseInt(response.headers.get('X-RateLimit-Remaining') ?? '0', 10),
rateLimitReset: response.headers.get('X-RateLimit-Reset') ?? '',
};
// Log for dashboarding
console.log('[MIRO_PERF]', JSON.stringify(metrics));
const data = response.status !== 204 ? await response.json() : null;
return { data: data as T, metrics };
}
| Technique | Impact | Effort | When to Use |
|---|---|---|---|
| Type-filtered queries | High | Low | Always — ?type=sticky_note reduces payload |
| LRU cache | High | Low | Read-heavy workloads |
| Redis cache + webhook invalidation | Very High | Medium | Multi-instance deployments |
| Controlled concurrency | High | Low | Bulk create/update operations |
| Connection pooling | Medium | Low | High request volume |
| Cursor pagination | High | Low | Always — avoid loading full board |
| Issue | Cause | Solution |
|---|---|---|
| Cache stale data | Long TTL + frequent edits | Shorten TTL or use webhook invalidation |
| Concurrency errors | Too many parallel requests | Reduce concurrency in PQueue |
| Connection pool exhausted | Max sockets too low | Increase maxSockets |
| Pagination cursor expired | Long gap between pages | Re-start pagination from beginning |
For cost optimization, see miro-cost-tuning.