如何异步消费由servant的StreamGenerators生成的流端点?
Alright, let's walk through how to consume a servant StreamGenerator endpoint asynchronously with JavaScript—this is something I’ve implemented a few times for frontend apps talking to Haskell backend services, so I’ll share the most reliable approaches.
First, it’s important to note that servant’s StreamGenerator typically serves data using Server-Sent Events (SSE) or chunked transfer encoding. SSE is the most common setup since it’s designed for server-to-client streaming and has native JavaScript APIs to handle it.
The Fetch API gives you low-level control over the stream, which is great if you need custom headers, authentication, or want to handle chunked data directly. Here’s a complete example:
async function consumeServantStream() { try { const response = await fetch('/your-stream-endpoint', { headers: { 'Accept': 'text/event-stream', // Critical: tells servant to send SSE-formatted data // Add auth headers if needed, e.g.: // 'Authorization': 'Bearer your-auth-token' } }); if (!response.ok) { throw new Error(`Request failed with status: ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); while (true) { const { done, value } = await reader.read(); if (done) break; // Decode the binary chunk to text const chunk = decoder.decode(value, { stream: true }); // Parse SSE-formatted lines (servant sends data prefixed with "data: ") const lines = chunk.split('\n').filter(line => line.trim() !== ''); for (const line of lines) { if (line.startsWith('data:')) { const rawData = line.slice(5).trim(); try { // Parse JSON (adjust if your stream sends plain text instead) const parsedData = JSON.parse(rawData); console.log('Received stream data:', parsedData); // Run your async processing logic here await processStreamData(parsedData); } catch (parseErr) { console.error('Failed to parse stream data:', parseErr); } } } } console.log('Stream completed successfully'); } catch (err) { console.error('Stream consumption failed:', err); } } // Example async processing function (customize this!) async function processStreamData(data) { // Simulate an async task (e.g., update UI, save to local storage, call another API) await new Promise(resolve => setTimeout(resolve, 150)); console.log('Processed stream entry:', data); } // Start consuming the stream consumeServantStream();
If your servant endpoint uses SSE (the standard for StreamGenerator), the EventSource API is purpose-built for this use case and requires less boilerplate:
function consumeWithEventSource() { const eventSource = new EventSource('/your-stream-endpoint', { // Note: Some browsers restrict custom headers in EventSource. // If you need auth, test this first—if it fails, fall back to the Fetch approach. headers: { // 'Authorization': 'Bearer your-auth-token' } }); // Handle incoming messages (matches servant's default SSE "data" events) eventSource.onmessage = async (event) => { try { const parsedData = JSON.parse(event.data); console.log('Received via EventSource:', parsedData); await processStreamData(parsedData); } catch (err) { console.error('Error handling stream message:', err); } }; // Handle stream errors (e.g., connection drops) eventSource.onerror = (error) => { console.error('EventSource connection error:', error); // Clean up and optionally retry eventSource.close(); }; // Handle stream closure eventSource.onclose = () => { console.log('Stream connection closed'); }; } // Start the stream consumeWithEventSource();
- CORS Configuration: If your frontend runs on a different domain than the servant backend, ensure the backend sets CORS headers to allow
text/event-streamcontent types and any custom headers you’re using (likeAuthorization). - Stream Format: Double-check your servant endpoint’s stream type. If it uses raw chunked JSON instead of SSE, skip the SSE parsing logic and directly
JSON.parseeach decoded chunk. - Order Guarantees: If stream order matters, use the Fetch approach with
awaitinside the loop to process chunks sequentially. EventSource triggers messages as they arrive, which can lead to out-of-order processing if your async tasks take variable time. - Resource Cleanup: Always close the reader or EventSource when you’re done (e.g., when a component unmounts in React/Vue) to avoid memory leaks. Use
reader.cancel()oreventSource.close().
内容的提问来源于stack exchange,提问作者Damian Nadales

