mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-11-02 02:19:43 +00:00
perf(app): reduce redundant data fetching
Share bounded database pools across API repositories, consolidate sync and chat queries, and make mobile refreshes cache-aware and foreground-aware. Add focused regression coverage, client freshness documentation, and repository agent role configuration.
This commit is contained in:
1 parent
9957fa43f2
commit
c7ee8a4851
81 files changed
+4456
-1400
No files matched your search
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { isForeground, subscribeToForeground } from '../lib/appForeground';
|
||||
|
||||
export interface UsePollingOptions {
|
||||
intervalMs: number;
|
||||
/** Caller-owned relevance: screen focus, a loaded id, a paired couple. */
|
||||
enabled?: boolean;
|
||||
/** Run once as soon as polling becomes active. Default true. */
|
||||
leading?: boolean;
|
||||
/** Ceiling for the exponential backoff applied after consecutive failures. */
|
||||
maxIntervalMs?: number;
|
||||
/**
|
||||
* Restarts the schedule when it changes, so a caller that switches subject —
|
||||
* a different chat, a different couple — gets an immediate run instead of
|
||||
* waiting out the interval left over from the previous one.
|
||||
*/
|
||||
resetKey?: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* A recurring task with the three properties every poller in this app needs and
|
||||
* none of them had: it stops while the app is backgrounded, it never runs twice
|
||||
* concurrently, and it backs off instead of hammering an API that is failing.
|
||||
*
|
||||
* The next run is scheduled after the previous one settles rather than on a fixed
|
||||
* `setInterval`, so a slow response delays the next request instead of queueing
|
||||
* one behind it. `task` is read through a ref, so a caller may pass a fresh
|
||||
* closure every render without restarting the schedule.
|
||||
*/
|
||||
export function usePolling(task: () => Promise<void>, options: UsePollingOptions): void {
|
||||
const { intervalMs, enabled = true, leading = true, maxIntervalMs = intervalMs * 8, resetKey } = options;
|
||||
const taskRef = useRef(task);
|
||||
taskRef.current = task;
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) return;
|
||||
let cancelled = false;
|
||||
let running = false;
|
||||
let failures = 0;
|
||||
let foreground = isForeground();
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
|
||||
const clear = () => {
|
||||
if (timer !== null) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
};
|
||||
const schedule = () => {
|
||||
if (cancelled || !foreground || timer !== null) return;
|
||||
const delay = Math.min(intervalMs * 2 ** failures, maxIntervalMs);
|
||||
timer = setTimeout(() => {
|
||||
timer = null;
|
||||
void run();
|
||||
}, delay);
|
||||
};
|
||||
const run = async () => {
|
||||
if (cancelled || running || !foreground) return;
|
||||
running = true;
|
||||
try {
|
||||
await taskRef.current();
|
||||
failures = 0;
|
||||
} catch {
|
||||
failures += 1;
|
||||
} finally {
|
||||
running = false;
|
||||
schedule();
|
||||
}
|
||||
};
|
||||
const unsubscribe = subscribeToForeground((next) => {
|
||||
if (next === foreground) return;
|
||||
foreground = next;
|
||||
if (!foreground) {
|
||||
clear();
|
||||
return;
|
||||
}
|
||||
// Back from the background: the cached data is as stale as the absence
|
||||
// was long, so catch up now rather than after another full interval.
|
||||
failures = 0;
|
||||
void run();
|
||||
});
|
||||
|
||||
if (leading) void run();
|
||||
else schedule();
|
||||
|
||||
return () => {
|
||||
cancelled = true;
|
||||
clear();
|
||||
unsubscribe();
|
||||
};
|
||||
}, [enabled, intervalMs, leading, maxIntervalMs, resetKey]);
|
||||
}
|
||||
Reference in new issue
Block a user