mirror of
https://github.com/signalapp/Signal-Desktop.git
synced 2026-04-19 16:08:34 +01:00
Rename files
This commit is contained in:
195
ts/components/fun/data/infinite.std.ts
Normal file
195
ts/components/fun/data/infinite.std.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright 2025 Signal Messenger, LLC
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { drop } from '../../../util/drop.std.js';
|
||||
import { createLogger } from '../../../logging/log.std.js';
|
||||
import * as Errors from '../../../types/errors.std.js';
|
||||
import { strictAssert } from '../../../util/assert.std.js';
|
||||
|
||||
const log = createLogger('infinite');
|
||||
|
||||
export type InfiniteQueryLoader<Query, Page> = (
|
||||
query: Query,
|
||||
previousPage: Page | null,
|
||||
signal: AbortSignal
|
||||
) => Promise<Page>;
|
||||
|
||||
export type InfiniteQueryOptions<Query, Page> = Readonly<{
|
||||
/** Important! Query must be memoized */
|
||||
query: Query;
|
||||
loader: InfiniteQueryLoader<Query, Page>;
|
||||
hasNextPage: (query: Query, page: Page) => boolean;
|
||||
}>;
|
||||
|
||||
export type InfiniteQueryState<Query, Page> = Readonly<{
|
||||
query: Query;
|
||||
pending: boolean;
|
||||
rejected: boolean;
|
||||
pages: ReadonlyArray<Page>;
|
||||
hasNextPage: boolean;
|
||||
}>;
|
||||
|
||||
export type InfiniteQueryApi<Query, Page> = Readonly<{
|
||||
queryState: InfiniteQueryState<Query, Page>;
|
||||
fetchNextPage: () => void;
|
||||
revalidate: () => void;
|
||||
}>;
|
||||
|
||||
export function useInfiniteQuery<Query, Page>(
|
||||
options: InfiniteQueryOptions<Query, Page>
|
||||
): InfiniteQueryApi<Query, Page> {
|
||||
const loaderRef = useRef(options.loader);
|
||||
const hasNextPageRef = useRef(options.hasNextPage);
|
||||
useEffect(() => {
|
||||
loaderRef.current = options.loader;
|
||||
hasNextPageRef.current = options.hasNextPage;
|
||||
}, [options.loader, options.hasNextPage]);
|
||||
|
||||
/**
|
||||
* This is used to abort both the first page and the next page fetchers
|
||||
* when the query changes.
|
||||
*/
|
||||
const querySignalRef = useRef<AbortSignal | null>(null);
|
||||
|
||||
const [edition, setEdition] = useState(0);
|
||||
const [state, setState] = useState<InfiniteQueryState<Query, Page>>({
|
||||
query: options.query,
|
||||
pending: true,
|
||||
rejected: false,
|
||||
pages: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
|
||||
const stateRef = useRef(state);
|
||||
const update = useCallback((next: InfiniteQueryState<Query, Page>) => {
|
||||
stateRef.current = next;
|
||||
setState(next);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
querySignalRef.current = signal;
|
||||
|
||||
let pendingStatusTimer: NodeJS.Timeout;
|
||||
|
||||
async function firstPageFetcher() {
|
||||
// Show pending state faster if results are empty
|
||||
const isEmpty = stateRef.current.pages.length === 0;
|
||||
const showPendingStateDelay = isEmpty ? 50 : 300;
|
||||
|
||||
pendingStatusTimer = setTimeout(() => {
|
||||
update({
|
||||
query: options.query,
|
||||
pending: true,
|
||||
rejected: false,
|
||||
pages: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
}, showPendingStateDelay);
|
||||
|
||||
try {
|
||||
const firstPage = await loaderRef.current(options.query, null, signal);
|
||||
if (!signal.aborted) {
|
||||
update({
|
||||
query: options.query,
|
||||
pending: false,
|
||||
rejected: false,
|
||||
pages: [firstPage],
|
||||
hasNextPage: hasNextPageRef.current(options.query, firstPage),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
update({
|
||||
...stateRef.current,
|
||||
pending: false,
|
||||
});
|
||||
} else {
|
||||
log.error('Error fetching first page', Errors.toLogFormat(error));
|
||||
update({
|
||||
query: options.query,
|
||||
pending: false,
|
||||
rejected: true,
|
||||
pages: [],
|
||||
hasNextPage: false,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(pendingStatusTimer);
|
||||
}
|
||||
}
|
||||
|
||||
drop(firstPageFetcher());
|
||||
|
||||
return () => {
|
||||
clearTimeout(pendingStatusTimer);
|
||||
controller.abort();
|
||||
};
|
||||
}, [options.query, edition, update]);
|
||||
|
||||
const fetchNextPage = useCallback(() => {
|
||||
strictAssert(
|
||||
querySignalRef.current,
|
||||
'Should have abort controller from first page fetcher'
|
||||
);
|
||||
|
||||
if (querySignalRef.current.aborted) {
|
||||
return;
|
||||
}
|
||||
|
||||
const signal = querySignalRef.current;
|
||||
|
||||
async function nextPageFetcher() {
|
||||
// Show pending state immediately
|
||||
update({
|
||||
...stateRef.current,
|
||||
pending: true,
|
||||
});
|
||||
|
||||
const { query, pages } = stateRef.current;
|
||||
try {
|
||||
const prevPage = pages.at(-1);
|
||||
strictAssert(prevPage, 'Expected previous resolved page');
|
||||
const nextPage = await loaderRef.current(query, prevPage, signal);
|
||||
if (!signal.aborted) {
|
||||
update({
|
||||
query,
|
||||
pending: false,
|
||||
rejected: false,
|
||||
pages: [...pages, nextPage],
|
||||
hasNextPage: hasNextPageRef.current(query, nextPage),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (signal.aborted) {
|
||||
update({
|
||||
...stateRef.current,
|
||||
pending: false,
|
||||
});
|
||||
} else {
|
||||
log.error('Error fetching next page', Errors.toLogFormat(error));
|
||||
update({
|
||||
query,
|
||||
pending: false,
|
||||
rejected: true,
|
||||
pages,
|
||||
hasNextPage: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(nextPageFetcher());
|
||||
}, [update]);
|
||||
|
||||
const revalidate = useCallback(() => {
|
||||
setEdition(prevEdition => prevEdition + 1);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
queryState: state,
|
||||
fetchNextPage,
|
||||
revalidate,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user