feat: Add popular content per locale (#14939)

This commit is contained in:
Sivin Varghese
2026-07-17 14:43:42 +05:30
committed by GitHub
parent 5af26e45a9
commit 331875cdaa
29 changed files with 1208 additions and 33 deletions
@@ -0,0 +1,120 @@
import { effectScope } from 'vue';
import { useAbortableRequest } from '../useAbortableRequest';
// Resolves when the request "completes", rejects like axios does when the
// signal is aborted mid-flight.
const abortableRunner =
(value, { fail = false } = {}) =>
signal =>
new Promise((resolve, reject) => {
signal.addEventListener('abort', () => {
const error = new Error('canceled');
error.name = 'CanceledError';
reject(error);
});
// Defer so a follow-up `run`/`abort` can supersede this one first.
Promise.resolve().then(() => {
if (signal.aborted) return;
if (fail) {
reject(new Error('boom'));
return;
}
resolve(value);
});
});
describe('useAbortableRequest', () => {
it('passes a fresh signal to the runner and returns its result', async () => {
const { run } = useAbortableRequest();
let received = null;
const result = await run(signal => {
received = signal;
return Promise.resolve('ok');
});
expect(received).toBeInstanceOf(AbortSignal);
expect(received.aborted).toBe(false);
expect(result).toBe('ok');
});
it('toggles isPending around the request', async () => {
const { run, isPending } = useAbortableRequest();
expect(isPending.value).toBe(false);
const pending = run(() => Promise.resolve('done'));
expect(isPending.value).toBe(true);
await pending;
expect(isPending.value).toBe(false);
});
it('aborts the previous request when a new one starts', async () => {
const { run } = useAbortableRequest();
const first = run(abortableRunner('first'));
const second = run(abortableRunner('second'));
await expect(first).resolves.toBeUndefined();
await expect(second).resolves.toBe('second');
});
it('returns the onAbort value when a request is superseded', async () => {
const { run } = useAbortableRequest();
const first = run(abortableRunner('first'), { onAbort: null });
const second = run(abortableRunner('second'));
await expect(first).resolves.toBeNull();
await expect(second).resolves.toBe('second');
});
it('abort cancels the in-flight request and clears isPending', async () => {
const { run, abort, isPending } = useAbortableRequest();
const pending = run(abortableRunner('value'));
expect(isPending.value).toBe(true);
abort();
await expect(pending).resolves.toBeUndefined();
expect(isPending.value).toBe(false);
});
it('rethrows non-abort errors and clears isPending', async () => {
const { run, isPending } = useAbortableRequest();
await expect(run(abortableRunner(null, { fail: true }))).rejects.toThrow(
'boom'
);
expect(isPending.value).toBe(false);
});
it('aborts the in-flight request when its scope is disposed', async () => {
const scope = effectScope();
let request;
scope.run(() => {
request = useAbortableRequest();
});
const pending = request.run(abortableRunner('value'));
expect(request.isPending.value).toBe(true);
scope.stop();
await expect(pending).resolves.toBeUndefined();
expect(request.isPending.value).toBe(false);
});
it('keeps separate controllers per instance', async () => {
const a = useAbortableRequest();
const b = useAbortableRequest();
const first = a.run(abortableRunner('a'));
// Starting b's request must not abort a's.
const second = b.run(abortableRunner('b'));
await expect(first).resolves.toBe('a');
await expect(second).resolves.toBe('b');
});
});
@@ -0,0 +1,62 @@
import { getCurrentScope, onScopeDispose, ref } from 'vue';
export const isAbortError = error =>
error?.name === 'AbortError' ||
error?.name === 'CanceledError' ||
error?.code === 'ERR_CANCELED';
/**
* Keeps only the latest request alive. Starting a new `run` (or calling
* `abort`) cancels the previous request through its `AbortSignal`, so
* out-of-order responses can never overwrite fresher data.
*
* @example
* const { run, abort, isPending } = useAbortableRequest();
* const results = await run(signal => api.search(query, { signal }));
*
* @returns {{
* run: (runner: (signal: AbortSignal) => Promise<any>, options?: { onAbort?: any }) => Promise<any>,
* abort: () => void,
* isPending: import('vue').Ref<boolean>,
* }}
* `run` resolves with the runner's value, or `options.onAbort` (default
* `undefined`) when the request was superseded. Non-abort errors are rethrown.
*/
export function useAbortableRequest() {
const isPending = ref(false);
let controller = null;
const abort = () => {
controller?.abort();
controller = null;
isPending.value = false;
};
const run = async (runner, { onAbort } = {}) => {
controller?.abort();
const currentController = new AbortController();
controller = currentController;
isPending.value = true;
try {
return await runner(currentController.signal);
} catch (error) {
if (currentController.signal.aborted || isAbortError(error))
return onAbort;
throw error;
} finally {
// Only the latest run owns the shared state; a superseded run leaves it
// for the run that replaced it.
if (controller === currentController) {
controller = null;
isPending.value = false;
}
}
};
// Cancel any in-flight request when the owning scope is disposed.
// Guarded so the composable can also be used outside an effect scope.
if (getCurrentScope()) onScopeDispose(abort);
return { run, abort, isPending };
}