AI Engineering
/
AI Application Integration
/

Implement a streaming state reducer

AI Application Integration · 2 to 3 hours

Implement a streaming state reducer

Implement a pure reducer that keeps a streamed answer consistent through stale runs, replays, gaps, and stops, and predict its transitions. Then review a fetch integration for decoding, framing, cancellation, and end-of-stream defects.

Medium
Hands-on

3 h

Step 1: The question

Implement reduceStream to the contract and predict the transition fixtures. Then find the defects in the fetch integration and describe a corrected flow with per-run AbortControllers. Explain how your design keeps stale-run events, duplicate deltas, and a stream that ends without a terminal event from corrupting the UI, and how the UI announces and labels each terminal state.

Define an event contract

Each server event carries a run ID and a one-based sequence number. A delta event appends text, done marks a complete answer, and error ends the run with a safe error message. The sequence covers every persisted event for that run, so a client can tell a replay from a gap.

The client starts a run through a local action. It accepts only events that belong to the active run and requires contiguous sequence numbers. An event that was already applied is ignored, and a gap marks the response interrupted rather than silently losing text.

The duplicate rule assumes the payload for a sequence number never changes; two different payloads under one number are a protocol violation for server-side integrity checks to catch.

Local Stop means the client stopped this attempt. It does not assert that the server cancelled every operation. A separate reconciliation step can later update the persisted run record from authoritative server state.

The reducer contract

The reducer receives the current state and one action and returns the next state. It is pure: no clock, no network, and no mutation of its inputs. It never throws: a bad sequence number becomes a failed state, not an exception. When an action changes nothing it returns the same state object, so useReducer can skip the re-render; any change returns a new object with exactly the five fields runId, status, text, lastSeq, and error.

type Status = "idle" | "streaming" | "complete" | "interrupted" | "stopped" | "failed";
type State = {
  runId: string | null;
  status: Status;
  text: string;
  lastSeq: number;
  error: string | null;
};
// seq is untrusted network data: the reducer validates it before use (rule 4).
type ServerEvent = { runId: string; seq: unknown } & (
  | { type: "delta"; text: string }
  | { type: "done" }
  | { type: "error"; message: string }
);
type Action =
  | { type: "start"; runId: string }
  | { type: "stop"; runId: string }
  | { type: "disconnect"; runId: string }
  | { type: "event"; event: ServerEvent };

const initialState: State = {
  runId: null, status: "idle", text: "", lastSeq: 0, error: null
};

declare function reduceStream(state: State, action: Action): State;

Apply these rules in order; the first one that matches decides the result.

  1. start returns a fresh streaming state for its run, { runId, status: "streaming", text: "", lastSeq: 0, error: null }, whatever the current state.
  2. Any other action whose run ID differs from state.runId, or any other action while status is not "streaming", returns the same state object. Complete, interrupted, stopped, and failed are therefore sticky until the next start.
  3. stop sets status "stopped", and disconnect sets "interrupted". Text, lastSeq, and error are kept.
  4. An event whose seq is not a safe integer of at least 1 sets status "failed" with error "Invalid event sequence".
  5. An event with seq <= lastSeq was already applied: return the same state object, not an equal copy.
  6. An event with seq !== lastSeq + 1 sets status "interrupted" with error "Missing event", keeping text and lastSeq.
  7. Otherwise apply the event and set lastSeq to its seq: delta appends its text, done sets "complete", and error sets "failed" with the event's message as the error.

A gap ends live accumulation on purpose. Reconnecting needs a separate snapshot or resume operation (the Hard question in this topic), not more events sent into an interrupted state.

The Python version is reduce_stream(state, action) on dictionaries, with run_id and last_seq in place of runId and lastSeq. Everything else, including the status and error strings, is identical. In Python, seq must be an int that is not a bool, from 1 to 2**53 - 1.

Constraints

  • The reducer is pure and synchronous: it returns the same object when nothing changes, a new object otherwise, and never mutates its inputs
  • Apply the seven rules in the stated order, with the exact error strings "Invalid event sequence" and "Missing event"
  • Events reach the reducer already decoded, framed, and validated; decoding, framing, and cancellation belong to the integration

Deliverables

  • A reducer that passes the linked practice problem, and your predicted final state for every transition case
  • How run identity and sequence numbers keep stale, duplicate, and out-of-order events from changing the answer
  • The defects in the integration and a corrected flow: decoding, framing, validation, and the end of the stream
  • Per-run cancellation for Stop, replacement, and unmount, and which handler may update which part of the UI
  • Status announcements, labels for partial answers, and safe rendering for each terminal state

Step 2: Hints

Optional. Each hint gives a little more away.

Step 3: Work through it

Code exercise

Streaming state reducer

Implement reduceStream (Python reduce_stream) to the contract above. Apply the seven rules in order, return the same object when nothing changes, and never mutate the input state or action.

Expected

The linked practice problem writes its starting states as literals (it does not use your initialState), applies actions through your reducer, and compares results with toEqual (Python ==). Its six tests cover the nine transition scenarios from the guide as one table (T1 to T9 below, with the same starting states), applied delta, done, and error events, actions for another run or a finished run returning the same object, a replay (seq <= lastSeq) returning the same object rather than an equal copy (toBe in JavaScript, is in Python), gaps, invalid sequence values such as 0, 1.5, true, and 2**53, and a start that always resets while the inputs stay unmodified.

Open coding workspace

Limitations

The reducer trusts that a runtime validator already checked event types, field types, string sizes, and the run-ID format; TypeScript types alone do not validate network data. It checks sequence numbers defensively but is not a protocol parser: it does not decode bytes, abort requests, or reconcile with the server. It treats a gap as the end of live accumulation, so reconnecting needs a separate snapshot or resume operation, and its duplicate rule assumes the payload for a sequence number never changes.

Predict the outcome

Predict the transitions

Each case is an independent scenario: apply its actions, in order, to its starting state with the seven rules, and predict the final state before you reveal it. The starting states, written as { runId, status, text, lastSeq, error }, are idle = { null, "idle", "", 0, null }, r1Hello = { "R1", "streaming", "Hello", 1, null }, r1Stopped = { "R1", "stopped", "Hello", 1, null }, and r2Complete = { "R2", "complete", "Hi", 2, null }. Shorthand: start R1, stop R1, and disconnect R1 are actions for run R1. R1 delta 1 "Hello" is an event with run ID R1, seq 1, type delta, and text "Hello". R1 done 2 and R1 error 2 "Upstream timeout" are done and error events with seq 2.

CaseStarting stateActions, in orderrunIdstatustextlastSeqerrorLast action returns the same object?
T1idlestart R1??????
T2idlestart R1; R1 delta 1 "Hello"??????
T3r1HelloR1 delta 1 "Hello"??????
T4r1HelloR1 delta 3 " world"??????
T5r1Hellostop R1??????
T6r1Stoppedstart R2??????
T7r1Stoppedstart R2; R2 delta 1 "Hi"; R1 delta 2 " world"??????
T8idlestart R2; R2 delta 1 "Hi"; R2 done 2??????
T9r2Completedisconnect R2??????
T10idlestart R3; R3 delta 1 "Partial"; R3 error 2 "Upstream timeout"??????
T11idlestart R3; R3 delta 1 "Hi"; R3 delta 0 "x"??????
T12r1HelloR1 delta 3 " world"; start R2??????

Diagnose

Review a fetch integration

Suppose a coding-practice platform streams hints. The backend accepts a request, assigns a run ID, and answers with status 200, content type application/x-ndjson, and an x-run-id header. It then streams one event per line in the event format above, and every frame, including the last, ends with a newline. The hint panel shows the reducer's text and status, plus a spinner driven by setLoading. A teammate connected the reducer like this (showBanner displays a page-level error):

let controller: AbortController | null = null;

async function requestHint(
  prompt: string,
  dispatch: (action: Action) => void,
  setLoading: (on: boolean) => void
): Promise<void> {
  controller = new AbortController();
  setLoading(true);
  try {
    const response = await fetch("/api/hints", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ prompt }),
      signal: controller.signal
    });
    const runId = response.headers.get("x-run-id") ?? "pending";
    dispatch({ type: "start", runId });
    const reader = response.body!.getReader();
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      const chunk = new TextDecoder().decode(value);
      for (const line of chunk.split("\n")) {
        if (line.trim() !== "") {
          dispatch({ type: "event", event: JSON.parse(line) as ServerEvent });
        }
      }
    }
  } catch {
    showBanner("Hints are unavailable right now.");
  } finally {
    setLoading(false);
  }
}

function stopHint(): void {
  controller?.abort();
}
  1. Find at least five defects. For each one, give a concrete sequence of user actions or network behavior that triggers it, and what the learner sees.
  2. Describe the corrected flow from submit to the end of the stream: which controller each run uses, when start is dispatched, how bytes become validated events, what happens at EOF, and what Stop, replacement, and unmount do.
  3. Name one test, with a fake reader or a local fake server, that would catch each defect.

Kept in this browser. Saving your score below keeps a copy with it.

0 / 5000

Step 4: Reference answer

Step 5: Score yourself

Score each dimension 0, 1, or 2, then save the score to your history. Readiness thresholds are editorial estimates.

Grades your written answer against this rubric with your own AI provider key from Settings. It is a second opinion: the score you save is still yours.

State transitions
Identity and sequence handling
Transport parsing
Cancellation and races
Accessibility and error UX

0 of 10 · Scored 0 of 5 dimensions

Ready at 8 of 10, with no 0 in State transitions or Identity and sequence handling

Kept in this browser until you sign up, then it moves to your account.

Complete this level when the state tests pass and you can explain which guarantees still depend on the transport adapter and backend.

Your saved scores

Practice saves and mock interview answers on this question, newest first.

No saved scores yet. Score every dimension, then save.

Step 6: Follow-up questions

An interviewer may ask these next.

Why does each event carry both a run ID and a sequence number?

Why decode with a streaming decoder instead of decoding each read on its own?

What should the client do when the reader reaches EOF without a done or error event?

What can an outdated request's catch or finally handler break?

What should a status region announce while a response streams?

Step 7: Mark complete