Fetch a bounded batch while preserving order and partial results
Application and assignment
An account dashboard shows a row for each requested account. Its API returns details at different speeds, and one account lookup can fail. The screen still needs each result beside the correct account, including an error for the failed row.
Implement the bounded asynchronous mapper described below. Inputs and output slots keep their original order, while up to K calls run concurrently. The reference is supplied for comparison after your attempt. This controls simultaneous calls in one runtime, not requests per second across a fleet.
Contract and starting evidence
“A dashboard loads details for 20 accounts. Starting every request overloads the service. Run at most three calls at once, keep input order, and display an error for one account without discarding successes. What does failure mean?”
Constructed 35-minute session. Prerequisite: runtime model. Keep the reference (download file, source below) closed until the attempt is complete.
Read the supplied code · 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;
}
Original illustration:
and .
| Contract | Expected behavior |
|---|---|
| Inputs | Ordered items, async operation, positive integer K |
| Example | [a,b,c], b fails, c finishes first → [success(a),failure(b),success(c)] |
| Boundaries | Empty input → []; K=0 or fractional K → reject; peak active ≤ min(K,n) |
| Excluded | QPS, fleet admission and forced cancellation |
Trace three calls in reverse completion order; assign each input an output slot; create K workers, each claiming an index synchronously before any await; catch each item's failure into its own slot. The counter is safe under this event-loop model, not automatically under threads. Explain every await before running the code.
does this enforce 100 calls/second? Expected: no; concurrency and rate are different. Add clock-based admission for QPS. Follow-up: caller aborts but two calls ignore abort. Expected: define skipped unstarted items, retain active permits until work stops, and guard UI commits separately. A timeout cannot free a real socket.
Run from the repository root:
node --test curriculum/01-code/02-data-structures-algorithms/typescript.test.ts
Scheduling is O(n), result space O(n), active calls O(min(K,n)); elapsed time depends on service durations. Senior checks include partial failures, reverse completion and invalid K. Lead scope adds dependency budgets across replicas. Next: the threaded executor.