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

FieldValue
runIdThe active run's ID, or null before the first run
status"idle", "streaming", "complete", "interrupted", "stopped", or "failed"
textThe answer text accumulated so far
lastSeqThe sequence number of the last applied event, 0 before any
errornull, 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

ActionMeaning
{ 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:

  1. start always returns a fresh state, whatever the current state is: { runId: action.runId, status: "streaming", text: "", lastSeq: 0, error: null }.
  2. If the action's run ID (action.runId, or action.event.runId for an event) differs from state.runId, or state.status is not "streaming", return state itself. Stale runs are ignored, and complete, interrupted, stopped, and failed are final until the next start.
  3. stop sets status to "stopped", and disconnect sets it to "interrupted". text, lastSeq, and error are kept.
  4. If seq is not a safe integer of at least 1 (Number.isSafeInteger(seq) && seq >= 1), set status to "failed" and error to "Invalid event sequence".
  5. If seq <= lastSeq, the event is a replay: return state itself.
  6. If seq !== lastSeq + 1, an event is missing: set status to "interrupted" and error to "Missing event", and keep text and lastSeq.
  7. Otherwise apply the event and set lastSeq to seq: delta appends its text, done sets status to "complete", and error sets status to "failed" and error to the event's message.

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.

NameState
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 }
#ScenarioStartActions
1Start R1idlestart R1
2First deltaidlestart R1; delta R1 seq 1 "Hello"
3Replay of seq 1r1Hellodelta R1 seq 1 "Hello"
4Seq 3 before seq 2r1Hellodelta R1 seq 3 " world"
5Stop while streamingr1Hellostop R1
6Start R2 after stopping R1r1Stoppedstart R2
7Late R1 event while R2 is activer1Stoppedstart R2; delta R2 seq 1 "Hi"; delta R1 seq 2 " world"
8Delta, then doneidlestart R2; delta R2 seq 1 "Hi"; done R2 seq 2
9Disconnect after completer2Completedisconnect 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

JavaScript
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:

JavaScriptPython
reduceStream(state, action)reduce_stream(state, action)
runIdrun_id
lastSeqlast_seq
nullNone
status, text, error, type, seq, event, messageunchanged

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 text and an error's message are strings, and a delta's text may be empty.
  • seq can be any value: an integer, a negative or fractional number, NaN, a number above 2**53 - 1, a boolean, a string, or null.
  • Inputs may be frozen with Object.freeze. Never assign to state, action, or action.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.

MEDIUM
AI Engineering
Streaming Responses
State Machine
Frontend
Intermediate

0 views

Solution

Hints

Hint 1
Hint 2
Premium
Hint 3
Premium