- Fix EventSource is not defined error in queue dashboard - Add browser guards for all EventSource usage - Replace static constants (EventSource.OPEN/CLOSED) with numeric values - Fix setInterval SSR violation in LLM health indicator - Replace $effect anti-pattern with onMount in share page - Add comprehensive SvelteKit SSR best practices documentation - Add SSR audit and testing verification All changes follow SvelteKit best practices and are verified against official documentation. Production build succeeds with no SSR errors. Closes: FixEventSourceSSR See: docs/outcomes/FixEventSourceSSR.md
13 KiB
InstaRecipe API Documentation
This document describes the InstaRecipe API endpoints for the async queue-based recipe extraction system.
Base URL
All API endpoints are relative to your InstaRecipe instance:
https://your-instarecipe-instance.com/api
Authentication
Currently, no authentication is required for API access. This may change in future versions.
Content Type
All requests should use Content-Type: application/json unless otherwise specified.
Error Handling
All endpoints return standardized error responses:
{
"error": "Error type",
"message": "Human-readable error message",
"details": { /* Additional error context */ }
}
HTTP status codes follow REST conventions:
200- Success201- Created400- Bad Request (invalid input)404- Not Found409- Conflict (e.g., cannot retry pending item)410- Gone (deprecated endpoint)500- Internal Server Error
Queue Management Endpoints
POST /api/queue
Enqueue an Instagram URL for async processing.
Request:
{
"url": "https://instagram.com/p/abc123"
}
Response (201 Created):
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://instagram.com/p/abc123",
"status": "pending",
"phases": [
{
"name": "extraction",
"status": "pending",
"progress": 0
},
{
"name": "parsing",
"status": "pending",
"progress": 0
},
{
"name": "uploading",
"status": "pending",
"progress": 0
}
],
"createdAt": "2024-12-21T10:30:00Z",
"updatedAt": "2024-12-21T10:30:00Z"
}
Errors:
400- Invalid Instagram URL format400- Missing or invalid URL parameter
GET /api/queue
List queue items with optional filtering, pagination, and sorting.
Query Parameters:
status(optional): Filter by status (pending,in_progress,success,error,unhealthy)limit(optional): Number of items to return (default: 50, max: 100)offset(optional): Number of items to skip (default: 0)sort(optional): Sort field (createdAt,updatedAt,status) (default:createdAt)order(optional): Sort order (asc,desc) (default:desc)
Examples:
GET /api/queue # All items
GET /api/queue?status=error # Failed items only
GET /api/queue?limit=10&offset=20 # Pagination
GET /api/queue?sort=status&order=asc # Sort by status
Response (200 OK):
{
"items": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://instagram.com/p/abc123",
"status": "success",
"phases": [
{
"name": "extraction",
"status": "completed",
"startedAt": "2024-12-21T10:30:01Z",
"completedAt": "2024-12-21T10:30:15Z",
"progress": 100
},
{
"name": "parsing",
"status": "completed",
"startedAt": "2024-12-21T10:30:15Z",
"completedAt": "2024-12-21T10:30:25Z",
"progress": 100
},
{
"name": "uploading",
"status": "completed",
"startedAt": "2024-12-21T10:30:25Z",
"completedAt": "2024-12-21T10:30:30Z",
"progress": 100
}
],
"results": {
"recipe": {
"name": "Chocolate Chip Cookies",
"description": "Delicious homemade cookies",
"servings": 24,
"ingredients": [
{
"food": "flour",
"amount": 2.25,
"unit": "cups"
}
],
"steps": [
{
"instruction": "Preheat oven to 375°F",
"time": 5
}
],
"keywords": ["cookies", "dessert", "chocolate"],
"image": "https://instagram.com/image.jpg"
},
"tandoorUrl": "https://tandoor.example.com/recipe/123",
"extractedText": "Raw extracted text...",
"thumbnail": "https://instagram.com/thumbnail.jpg"
},
"createdAt": "2024-12-21T10:30:00Z",
"updatedAt": "2024-12-21T10:30:30Z"
}
],
"total": 42,
"hasMore": true
}
GET /api/queue/{id}
Get details for a specific queue item.
Path Parameters:
id: Queue item UUID
Response (200 OK): Returns the same queue item structure as in the list response.
Errors:
400- Invalid UUID format404- Queue item not found
POST /api/queue/{id}/retry
Retry a failed queue item. Only items with error or unhealthy status can be retried.
Path Parameters:
id: Queue item UUID
Response (200 OK):
{
"success": true,
"message": "Item queued for retry",
"item": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"status": "pending",
"updatedAt": "2024-12-21T11:00:00Z"
}
}
Errors:
400- Invalid UUID format404- Queue item not found409- Cannot retry item with current status (onlyerrorandunhealthycan be retried)
Real-time Updates
GET /api/queue/stream
Server-Sent Events (SSE) endpoint for real-time queue updates.
Query Parameters:
itemId(optional): Filter updates for specific itemstatus(optional): Filter updates by status
Headers:
Accept: text/event-stream
Cache-Control: no-cache
Response: SSE stream with the following event types:
connection
Sent when connection is established:
event: connection
data: {"message": "Connected to queue stream", "timestamp": "2024-12-21T10:30:00Z"}
queue-update
Sent when queue item status changes:
event: queue-update
data: {
"itemId": "550e8400-e29b-41d4-a716-446655440000",
"status": "in_progress",
"timestamp": "2024-12-21T10:30:01Z",
"progress": [
{
"name": "extraction",
"status": "in_progress",
"startedAt": "2024-12-21T10:30:01Z",
"progress": 45
}
]
}
ping
Keep-alive ping sent every 30 seconds:
event: ping
data: {"timestamp": "2024-12-21T10:30:30Z"}
Usage Examples:
JavaScript:
const eventSource = new EventSource('/api/queue/stream');
eventSource.addEventListener('connection', (event) => {
console.log('Connected:', JSON.parse(event.data));
});
eventSource.addEventListener('queue-update', (event) => {
const update = JSON.parse(event.data);
console.log('Queue update:', update);
updateUI(update);
});
eventSource.addEventListener('ping', (event) => {
console.log('Keep-alive ping');
});
eventSource.onerror = (error) => {
console.error('SSE error:', error);
// Reconnect logic here
};
curl:
curl -N -H "Accept: text/event-stream" \
"https://your-instance.com/api/queue/stream?itemId=550e8400-e29b-41d4-a716-446655440000"
Push Notifications
GET /api/notifications/vapid-key
Get the VAPID public key required for push notification subscriptions.
Response (200 OK):
{
"publicKey": "BDummyPublicKeyForDevelopment...",
"applicationServerKey": "BDummyPublicKeyForDevelopment..."
}
POST /api/notifications/subscribe
Subscribe to push notifications for queue processing updates.
Request:
{
"subscription": {
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
"keys": {
"p256dh": "BIBn3E_YUVpW5f6_Eq_GH...",
"auth": "tBiH_Y1nPSuVh7TRMhcf..."
}
},
"clientId": "unique-client-identifier"
}
Response (200 OK):
{
"success": true,
"message": "Successfully subscribed to push notifications",
"subscriptionCount": 5
}
Errors:
400- Invalid subscription object or missing clientId
DELETE /api/notifications/subscribe
Unsubscribe from push notifications.
Request:
{
"clientId": "unique-client-identifier"
}
Response (200 OK):
{
"success": true,
"message": "Successfully unsubscribed from push notifications",
"subscriptionCount": 4
}
Legacy Endpoints (Deprecated)
POST /api/extract ⚠️ DEPRECATED
Status: 410 Gone
This synchronous extraction endpoint is deprecated. Use POST /api/queue instead.
Migration:
// ❌ Old synchronous approach
const response = await fetch('/api/extract', {
method: 'POST',
body: JSON.stringify({ url })
});
const result = await response.json(); // Wait 30-60 seconds
// ✅ New async queue approach
const response = await fetch('/api/queue', {
method: 'POST',
body: JSON.stringify({ url })
});
const queueItem = await response.json(); // Immediate response
POST /api/extract-stream ⚠️ DEPRECATED
Status: 410 Gone
This SSE endpoint is deprecated. Use POST /api/queue + GET /api/queue/stream instead.
Migration:
// ❌ Old approach
const response = await fetch('/api/extract-stream', {
method: 'POST',
body: JSON.stringify({ url })
});
// ✅ New approach
const queueResponse = await fetch('/api/queue', {
method: 'POST',
body: JSON.stringify({ url })
});
const item = await queueResponse.json();
const eventSource = new EventSource(`/api/queue/stream?itemId=${item.id}`);
Data Models
QueueItem
interface QueueItem {
id: string; // UUID v4
url: string; // Instagram URL
status: 'pending' | 'in_progress' | 'success' | 'error' | 'unhealthy';
phases: Array<{
name: 'extraction' | 'parsing' | 'uploading';
status: 'pending' | 'in_progress' | 'completed' | 'error';
startedAt?: string; // ISO 8601 timestamp
completedAt?: string; // ISO 8601 timestamp
progress?: number; // 0-100
}>;
results?: {
recipe?: Recipe; // Structured recipe data
tandoorUrl?: string; // Tandoor recipe URL
extractedText?: string; // Raw extracted text
thumbnail?: string; // Image URL
};
error?: string; // Error message
createdAt: string; // ISO 8601 timestamp
updatedAt: string; // ISO 8601 timestamp
}
Recipe
interface Recipe {
name: string;
description?: string;
servings?: number;
prepTime?: number; // Minutes
cookTime?: number; // Minutes
totalTime?: number; // Minutes
ingredients: Array<{
food: string;
amount?: number;
unit?: string;
}>;
steps: Array<{
instruction: string;
time?: number; // Minutes
}>;
keywords?: string[]; // Recipe tags
image?: string; // Image URL
nutrition?: { // Nutritional information
calories?: number;
protein?: number;
carbs?: number;
fat?: number;
};
}
Rate Limiting
Currently, no rate limiting is enforced, but this may change in future versions. Consider implementing client-side rate limiting to avoid overwhelming the service.
WebSocket Alternative
For applications that cannot use Server-Sent Events, consider polling the GET /api/queue/{id} endpoint every 5-10 seconds for status updates.
Error Recovery
When implementing clients, consider these error recovery strategies:
- Network Errors: Retry with exponential backoff
- SSE Connection Drops: Automatically reconnect after 5-10 seconds
- Queue Item Failures: Present retry option to users
- Push Notification Failures: Gracefully degrade to polling
Examples
Complete Processing Workflow
async function processInstagramUrl(url) {
try {
// 1. Enqueue URL
const queueResponse = await fetch('/api/queue', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const queueItem = await queueResponse.json();
console.log('Enqueued:', queueItem.id);
// 2. Listen for real-time updates
const eventSource = new EventSource(`/api/queue/stream?itemId=${queueItem.id}`);
return new Promise((resolve, reject) => {
eventSource.addEventListener('queue-update', (event) => {
const update = JSON.parse(event.data);
if (update.status === 'success') {
eventSource.close();
resolve(update.results);
} else if (update.status === 'error') {
eventSource.close();
reject(new Error(update.error));
}
// Handle progress updates
console.log('Progress:', update.progress);
});
eventSource.onerror = (error) => {
eventSource.close();
reject(error);
};
});
} catch (error) {
console.error('Processing failed:', error);
throw error;
}
}
// Usage
processInstagramUrl('https://instagram.com/p/abc123')
.then(results => {
console.log('Recipe extracted:', results.recipe);
if (results.tandoorUrl) {
console.log('Uploaded to Tandoor:', results.tandoorUrl);
}
})
.catch(error => {
console.error('Extraction failed:', error.message);
});
For more examples and integration guides, see the Migration Documentation.