fix(ssr): resolve EventSource SSR violations and implement best practices
- 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
This commit is contained in:
548
docs/API.md
Normal file
548
docs/API.md
Normal file
@@ -0,0 +1,548 @@
|
||||
# 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:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "Error type",
|
||||
"message": "Human-readable error message",
|
||||
"details": { /* Additional error context */ }
|
||||
}
|
||||
```
|
||||
|
||||
HTTP status codes follow REST conventions:
|
||||
- `200` - Success
|
||||
- `201` - Created
|
||||
- `400` - Bad Request (invalid input)
|
||||
- `404` - Not Found
|
||||
- `409` - 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:**
|
||||
```json
|
||||
{
|
||||
"url": "https://instagram.com/p/abc123"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (201 Created):**
|
||||
```json
|
||||
{
|
||||
"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 format
|
||||
- `400` - 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:**
|
||||
```bash
|
||||
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):**
|
||||
```json
|
||||
{
|
||||
"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 format
|
||||
- `404` - 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):**
|
||||
```json
|
||||
{
|
||||
"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 format
|
||||
- `404` - Queue item not found
|
||||
- `409` - Cannot retry item with current status (only `error` and `unhealthy` can 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 item
|
||||
- `status` (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:**
|
||||
```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:**
|
||||
```bash
|
||||
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):**
|
||||
```json
|
||||
{
|
||||
"publicKey": "BDummyPublicKeyForDevelopment...",
|
||||
"applicationServerKey": "BDummyPublicKeyForDevelopment..."
|
||||
}
|
||||
```
|
||||
|
||||
### POST /api/notifications/subscribe
|
||||
|
||||
Subscribe to push notifications for queue processing updates.
|
||||
|
||||
**Request:**
|
||||
```json
|
||||
{
|
||||
"subscription": {
|
||||
"endpoint": "https://fcm.googleapis.com/fcm/send/...",
|
||||
"keys": {
|
||||
"p256dh": "BIBn3E_YUVpW5f6_Eq_GH...",
|
||||
"auth": "tBiH_Y1nPSuVh7TRMhcf..."
|
||||
}
|
||||
},
|
||||
"clientId": "unique-client-identifier"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```json
|
||||
{
|
||||
"clientId": "unique-client-identifier"
|
||||
}
|
||||
```
|
||||
|
||||
**Response (200 OK):**
|
||||
```json
|
||||
{
|
||||
"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:**
|
||||
```javascript
|
||||
// ❌ 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:**
|
||||
```javascript
|
||||
// ❌ 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
|
||||
|
||||
```typescript
|
||||
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
|
||||
|
||||
```typescript
|
||||
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:
|
||||
|
||||
1. **Network Errors**: Retry with exponential backoff
|
||||
2. **SSE Connection Drops**: Automatically reconnect after 5-10 seconds
|
||||
3. **Queue Item Failures**: Present retry option to users
|
||||
4. **Push Notification Failures**: Gracefully degrade to polling
|
||||
|
||||
## Examples
|
||||
|
||||
### Complete Processing Workflow
|
||||
|
||||
```javascript
|
||||
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](/docs/MIGRATION.md).
|
||||
Reference in New Issue
Block a user