Streaming
Every AI application deals with streaming data. LLM responses arrive token by token, progress updates flow continuously, and data accumulates over time. Most frameworks treat streaming as infrastructure you wire up yourself—WebSocket event handlers, state synchronization logic, cleanup callbacks. You spend your time building plumbing instead of features. Idyllic takes a different approach. When you declare a field withstream<T>, you’re telling the framework that this field receives incremental updates that should be pushed to clients in real-time. The framework sets up WebSocket infrastructure, handles buffering, and ensures updates arrive in order. You just call append() with each chunk.
The Streaming Interface
A streamable value has different interfaces for reading and writing. Clients see a read-only interface; the server sees mutation methods for appending data, completing the stream, or signaling failures.Client Interface
current property holds the accumulated value, updating automatically as chunks arrive. The status indicates lifecycle state: 'idle' means not started, 'streaming' means data is arriving, 'done' means completed successfully, 'error' means failure occurred.
The value() method returns a promise that resolves with the final accumulated value once streaming completes, or rejects if the stream encounters an error.
Server Interface
append(chunk)adds data incrementally. Strings concatenate, objects shallow-merge, arrays concatenate.set(value)replaces the entire value at once.complete()marks the stream as finished, resolving any pendingvalue()promises.fail(error)marks the stream as failed, rejecting any pendingvalue()promises.reset()clears accumulated value, sets status to'idle', prepares for reuse.
Writing to Streams
The typical pattern: reset the stream, append chunks as they arrive, mark complete when done.append() immediately updates current, broadcasts to all connected clients, and returns without blocking. Clients see their UI update progressively as tokens arrive.
Error Handling
Wrap streaming in try-catch and callfail() to communicate errors to clients:
fail(), status becomes 'error', pending value() promises reject, and current retains whatever partial content arrived before failure.
Replacing Values
Useset() when each update represents a complete new state rather than additional data:
Reading Streams in React
Switch on status to show appropriate UI for each lifecycle state:Awaiting Completion
When you need the final value rather than reacting to incremental updates:complete() is called, rejects when fail() is called, and remains pending during streaming.
Object and Array Streams
Object Streams
For objects,append() performs shallow merge. Each call merges only the properties you include:
Array Streams
For arrays,append() concatenates items to the end:
Patterns
Parallel Streams
Multiple streams can update simultaneously. Each operates independently:Await All Streams
Wait for multiple streams before proceeding:Conditional Streaming
Bypass streaming when data is cached:Edge Cases
| Scenario | Behavior |
|---|---|
| Client connects mid-stream | Receives accumulated value, continues receiving appends |
| Server errors mid-stream | Status becomes 'error', current contains partial content |
| Multiple clients | All receive same chunks in same order |
append() after complete | Throws error—reset first |
complete() twice | No-op (idempotent) |
FAQ
What happens if append() is called very fast?
Idyllic batches rapid updates for network efficiency. If you call append() 100 times in a tight loop, the framework may combine updates into fewer WebSocket messages. Clients still receive all content in order—batching is transparent.
Can I have multiple active streams on the same field?
No. Each stream field supports one active stream at a time. Callreset() to start a new stream. For multiple simultaneous streams, use separate fields.
What if I forget to call complete()?
The stream remains in 'streaming' status indefinitely. Any value() promises never resolve. Always ensure streaming code paths call either complete() or fail(). Consider a try-finally pattern.
What’s the difference between stream<T> and regular fields?
Regular fields synchronize their values when assigned but don’t have lifecycle semantics or incremental updates. Stream fields provide: current that updates progressively, status tracking, value() promises, and mutation methods. Use stream<T> when clients need to see data arriving progressively.
Next: Actions
How methods become typed RPC endpoints