Streaming State Reducer
Part of AI Engineering: Implement a streaming state reducer
Streaming State Reducer
A note-taking app has an assistant panel that streams answers from its backend. The server sends events for one run (one answer attempt) at a time, and the browser folds them into UI state with React's useReducer. The transport layer has already decoded the bytes, parsed each frame, and checked the field types, so the reducer receives well-formed actions. The one field it must still check is the sequence number: a replayed, skipped, or malformed sequence number is exactly what turns a partial answer into a corrupted one.
Implement reduceStream(state, action). It is a pure function: it returns the next state and never modifies state or action. When an action changes nothing, it returns the same state object, which lets React skip the re-render.
State
| Field | Value |
|---|---|
runId | The active run's ID, or null before the first run |
status | "idle", "streaming", "complete", "interrupted", "stopped", or "failed" |
text | The answer text accumulated so far |
lastSeq | The sequence number of the last applied event, 0 before any |
error | null, or a short message explaining a failure or interruption |
The app starts from { runId: null, status: "idle", text: "", lastSeq: 0, error: null }. Every state you return has exactly these five fields.
Actions
| Action | Meaning |
|---|---|
{ type: "start", runId } | The backend accepted a new run |
{ type: "stop", runId } | The user pressed Stop for that run |
{ type: "disconnect", runId } | The stream ended before a terminal event arrived |
{ type: "event", event } | A server event { runId, seq, type, text?, message? } |
A server event's type is "delta" (with the text to append), "done", or "error" (with a safe message). A run's sequence numbers start at 1 and increase by 1 for every event of that run.
Rules
Apply the first rule that matches:
startalways returns a fresh state, whatever the current state is:{ runId: action.runId, status: "streaming", text: "", lastSeq: 0, error: null }.- If the action's run ID (
action.runId, oraction.event.runIdfor an event) differs fromstate.runId, orstate.statusis not"streaming", returnstateitself. Stale runs are ignored, andcomplete,interrupted,stopped, andfailedare final until the nextstart. stopsetsstatusto"stopped", anddisconnectsets it to"interrupted".text,lastSeq, anderrorare kept.- If
seqis not a safe integer of at least 1 (Number.isSafeInteger(seq) && seq >= 1), setstatusto"failed"anderrorto"Invalid event sequence". - If
seq <= lastSeq, the event is a replay: returnstateitself. - If
seq !== lastSeq + 1, an event is missing: setstatusto"interrupted"anderrorto"Missing event", and keeptextandlastSeq. - Otherwise apply the event and set
lastSeqtoseq:deltaappends itstext,donesetsstatusto"complete", anderrorsetsstatusto"failed"anderrorto the event'smessage.
Rule 4 runs before rule 5 on purpose. In JavaScript, true <= 1, "1" <= 1, and null <= 1 are all true, so a malformed sequence number checked as a replay would be dropped silently instead of reported.
Fixture
The rows are independent. Each one starts from the named state and folds its actions in order.
| Name | State |
|---|---|
idle | { runId: null, status: "idle", text: "", lastSeq: 0, error: null } |
r1Hello | { runId: "R1", status: "streaming", text: "Hello", lastSeq: 1, error: null } |
r1Stopped | { runId: "R1", status: "stopped", text: "Hello", lastSeq: 1, error: null } |
r2Complete | { runId: "R2", status: "complete", text: "Hi", lastSeq: 2, error: null } |
| # | Scenario | Start | Actions |
|---|---|---|---|
| 1 | Start R1 | idle | start R1 |
| 2 | First delta | idle | start R1; delta R1 seq 1 "Hello" |
| 3 | Replay of seq 1 | r1Hello | delta R1 seq 1 "Hello" |
| 4 | Seq 3 before seq 2 | r1Hello | delta R1 seq 3 " world" |
| 5 | Stop while streaming | r1Hello | stop R1 |
| 6 | Start R2 after stopping R1 | r1Stopped | start R2 |
| 7 | Late R1 event while R2 is active | r1Stopped | start R2; delta R2 seq 1 "Hi"; delta R1 seq 2 " world" |
| 8 | Delta, then done | idle | start R2; delta R2 seq 1 "Hi"; done R2 seq 2 |
| 9 | Disconnect after complete | r2Complete | disconnect R2 |
Work out each row's final runId, status, text, lastSeq, and error from the rules before you run the tests: the first test folds all nine rows and compares every field.
Example
const idle = { runId: null, status: 'idle', text: '', lastSeq: 0, error: null };
let s = reduceStream(idle, { type: 'start', runId: 'R1' });
s = reduceStream(s, { type: 'event', event: { runId: 'R1', seq: 1, type: 'delta', text: 'Hel' } });
s = reduceStream(s, { type: 'event', event: { runId: 'R1', seq: 2, type: 'delta', text: 'lo' } });
s = reduceStream(s, { type: 'event', event: { runId: 'R1', seq: 3, type: 'error', message: 'Rate limit reached' } });
// { runId: 'R1', status: 'failed', text: 'Hello', lastSeq: 3, error: 'Rate limit reached' }The error event is applied like any other event: lastSeq advances to 3 and the partial text stays visible.
Python
Implement reduce_stream(state, action). States, actions, and events are dicts, and the keys are renamed:
| JavaScript | Python |
|---|---|
reduceStream(state, action) | reduce_stream(state, action) |
runId | run_id |
lastSeq | last_seq |
null | None |
status, text, error, type, seq, event, message | unchanged |
Return state itself (the same dict, so result is state) when nothing changes. Python has no Number.isSafeInteger, so rule 4 becomes: seq is an int but not a bool (because True == 1), and 1 <= seq <= 2**53 - 1 (because Python integers never overflow).
Constraints
- Run IDs are non-empty strings. A delta's
textand an error'smessageare strings, and a delta'stextmay be empty. seqcan be any value: an integer, a negative or fractional number,NaN, a number above2**53 - 1, a boolean, a string, ornull.- Inputs may be frozen with
Object.freeze. Never assign tostate,action, oraction.event.
Expected Complexity
O(1) work per action apart from concatenating the text for a delta, and O(1) extra space besides the returned state.
0 views
Solution
Hints
