Search race · the latest user intent winsLESSON 6.02 · 2 OF 7 IN CHAPTER
PART B / Frontend state and API integration
Step 107 of 252
LESSON 6.02 · 2 OF 7 IN CHAPTERHands-on

Search race · the latest user intent wins

The search box shows bookmarks returned by an API. Alice types cat, then changes it to car before the first request finishes. The server may complete those requests in either order. The screen should keep answering the current query, car.

You will implement the small coordinator that decides which response may update the view. A generation is just a counter assigned to each new search. It is not the time when a network response arrives. Start by following the two requests in this animation, then use the table to define success and failure behavior.

Two browser searches finish in reverse order. Only the response matching the latest request generation may update the screen.

“Alice searches ‘cat’, then ‘car’. Cat finishes last and replaces the car results. Some loaders ignore cancellation. Which response may change the screen?”

Constructed 30-minute session. Prerequisite: runtime model. Keep reference (download file, source below) and tests (download file, source below) separate from the initial attempt.

Read the supplied code · typescript.ts
reference · typescript.ts
/** Bounded workers: preserve input order; drain started work and report errors per item. */
export async function mapLimit<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<PromiseSettledResult<R>[]> {
  if (!Number.isInteger(limit) || limit < 1) throw new RangeError('positive integer limit required');
  const out: PromiseSettledResult<R>[] = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++; // No await between reading and claiming the index.
      try { out[index] = { status: 'fulfilled', value: await fn(items[index], index) }; }
      catch (reason) { out[index] = { status: 'rejected', reason }; }
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return out;
}

/** Aborting old work saves resources where supported. The generation guard protects correctness. */
export function latestOnly<T>(load: (query: string, signal: AbortSignal) => Promise<T>, render: (value: T) => void) {
  let generation = 0;
  let active: AbortController | undefined;
  return async (query: string): Promise<boolean> => {
    const own = ++generation;
    active?.abort();
    const controller = new AbortController();
    active = controller;
    try {
      const value = await load(query, controller.signal);
      if (own !== generation) return false;
      render(value);
      return true;
    } catch (error) {
      if (own !== generation) return false;
      throw error; // Current request failure must be visible to the caller/UI.
    }
  };
}

export class LRU<K, V> {
  private readonly values = new Map<K, V>();
  private readonly capacity: number;
  constructor(capacity: number) {
    if (!Number.isInteger(capacity) || capacity < 0) throw new RangeError('nonnegative integer capacity required');
    this.capacity = capacity;
  }
  get(key: K): V | undefined {
    if (!this.values.has(key)) return undefined;
    const value = this.values.get(key) as V;
    this.values.delete(key);
    this.values.set(key, value);
    return value;
  }
  put(key: K, value: V) {
    if (this.capacity === 0) return;
    this.values.delete(key);
    this.values.set(key, value);
    if (this.values.size > this.capacity) this.values.delete(this.values.keys().next().value as K);
  }
}

export type Bookmark = { id: string; title: string; version: number };
/** Apply a server response only if no newer local/server version is known. */
export function reconcile(current: Bookmark, incoming: Bookmark): Bookmark {
  if (current.id !== incoming.id) throw new Error('different entity');
  return incoming.version >= current.version ? incoming : current;
}
Read the supplied code · typescript.test.ts
tests · typescript.test.ts
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { mapLimit, latestOnly, LRU, reconcile } from './typescript.ts';

test('bounded concurrency, order, and rejection isolation', async () => {
  let active = 0, peak = 0;
  const result = await mapLimit([4,3,2,1],2,async value => {
    active++; peak=Math.max(active,peak);
    try {
      await new Promise(resolve=>setTimeout(resolve,value));
      if(value===3) throw new Error('test failure');
      return value*2;
    } finally { active--; }
  });
  assert.equal(peak,2);assert.equal(active,0);
  assert.deepEqual(result.map(x=>x.status==='fulfilled'?x.value:'error'),[8,'error',4,2]);
  assert.deepEqual(await mapLimit([],2,async x=>x),[]);
  await assert.rejects(mapLimit([1],0,async x=>x),RangeError);
});
test('stale result cannot render even if load ignores abort',async()=>{
  const pending: Record<string,(x:string)=>void> = {};
  const rendered:string[]=[];
  const search=latestOnly((q)=>new Promise<string>(r=>{pending[q]=r}),v=>rendered.push(v));
  const old=search('old'),fresh=search('new');
  pending.new('new');assert.equal(await fresh,true);
  pending.old('old');assert.equal(await old,false);
  assert.deepEqual(rendered,['new']);
});
test('current failure propagates',async()=>{
  const search=latestOnly(async()=>{throw new Error('offline')},()=>{});
  await assert.rejects(search('q'),/offline/);
});
test('LRU refresh, overwrite, and zero capacity',()=>{
  const c=new LRU<string,number>(2);c.put('a',1);c.put('b',2);c.get('a');c.put('c',3);
  assert.equal(c.get('b'),undefined);c.put('a',4);assert.equal(c.get('a'),4);
  const empty=new LRU(0);empty.put('a',1);assert.equal(empty.get('a'),undefined);
});
test('late response cannot regress version',()=>{
  const current={id:'a',title:'new',version:3};
  assert.equal(reconcile(current,{id:'a',title:'old',version:2}),current);
  assert.throws(()=>reconcile(current,{id:'b',title:'x',version:4}));
});

Original illustration: browser race animation and .

Contract Expected behavior
Worked input Request 1=cat; request 2=car; complete 2 then 1
Output Only car commits, even if request 1 ignores abort
Failure Current failure displays an error; obsolete failure cannot replace current state
Boundary A disposed view never commits
Excluded Debouncing, QPS and server-side cancellation guarantees
Diagram: Search race · the latest user intent wins

Restate “latest” as request generation, not completion time. Trace both completions; increment a generation per request; capture it before awaiting; commit only if it is still current. Abort obsolete work when supported, but retain the generation guard. Errors need the same guard as successes.

Diagram: Search race · the latest user intent wins

unmount before completion. Expected: abort and invalidate; test ignored abort. Follow-up: reduce calls while typing. Expected: debounce separately; retain generation correctness. Coordinator state is O(1) beyond payloads. Old network/server work may continue after the browser aborts.

Run from root: node --test curriculum/01-code/02-data-structures-algorithms/typescript.test.ts. The real bookmark slice adds browser empty/error/retry states, disposal and storage. Senior evidence requires reverse completion and current-error tests; lead scope adds budgets and client compatibility.