Syntax previewClick to edit
12345
export default function takeLatest(fn) {
// TODO: return a wrapped function that aborts previous calls and resolves only the latest
throw new Error('Not implemented');
}
Syntax previewClick to edit
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798
import takeLatest from './takeLatest';
function toAbortError(reason, message = 'Aborted') {
if (reason && reason.name === 'AbortError') return reason;
const err = new Error(reason && reason.message ? reason.message : message);
try { err.name = 'AbortError'; } catch { try { Object.defineProperty(err, 'name', { value: 'AbortError' }); } catch {} }
return err;
}
function delay(ms, value, signal) {
return new Promise((resolve, reject) => {
const id = setTimeout(() => resolve(value), ms);
if (signal) {
if (signal.aborted) {
clearTimeout(id);
return reject(toAbortError(signal.reason));
}
signal.addEventListener(
'abort',
() => {
clearTimeout(id);
reject(toAbortError(signal.reason));
},
{ once: true }
);
}
});
}
describe('takeLatest', () => {
test('resolves only the latest call', async () => {
const latest = takeLatest((signal, value, ms) => delay(ms, value, signal));
const p1 = latest('first', 30);
const p2 = latest('second', 5);
await expect(p1).rejects.toMatchObject({ name: 'AbortError' });
await expect(p2).resolves.toBe('second');
});
test('propagates errors from the latest call', async () => {
const latest = takeLatest(async () => {
throw new Error('boom');
});
await expect(latest()).rejects.toThrow('boom');
});
test('does not abort a controller after its call has completed', async () => {
const signals = [];
const latest = takeLatest(async (signal, value) => {
signals.push(signal);
return value;
});
await expect(latest('first')).resolves.toBe('first');
const completedSignal = signals[0];
await expect(latest('second')).resolves.toBe('second');
expect(completedSignal.aborted).toBe(false);
});
test('a stale finally cannot clear the newer in-flight controller', async () => {
const runs = [];
const latest = takeLatest((signal, value) => new Promise((resolve) => {
runs.push({ signal, value, resolve });
}));
const first = latest('first');
await Promise.resolve();
const second = latest('second');
await Promise.resolve();
runs[0].resolve('first');
await expect(first).rejects.toMatchObject({ name: 'AbortError' });
const third = latest('third');
await Promise.resolve();
expect(runs[1].signal.aborted).toBe(true);
runs[1].resolve('second');
runs[2].resolve('third');
await expect(second).rejects.toMatchObject({ name: 'AbortError' });
await expect(third).resolves.toBe('third');
});
test('aborts the previous controller immediately', async () => {
const latest = takeLatest((signal) => delay(20, 'ok', signal));
const p1 = latest();
await new Promise((r) => setTimeout(r, 1));
const p2 = latest();
await expect(p1).rejects.toMatchObject({ name: 'AbortError' });
await expect(p2).resolves.toBe('ok');
});
});
Run tests to see results.