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
@@ -10,7 +10,36 @@ export interface LiveDrawState {
|
||||
updated_by: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A change summary, not a payload: markers the client compares against the ones
|
||||
* it saw last time to decide which domain, if any, is worth refetching. It
|
||||
* replaces a periodic full refresh of profile, couple, matches, pending
|
||||
* questions, packs and unread counts with one small request.
|
||||
*/
|
||||
export interface SyncSummary {
|
||||
server_time: string;
|
||||
couple_id: string | null;
|
||||
profile_updated_at: string | null;
|
||||
partner_id: string | null;
|
||||
partner_updated_at: string | null;
|
||||
match_count: number;
|
||||
new_match_count: number;
|
||||
latest_match_at: string | null;
|
||||
pending_yours: number;
|
||||
pending_theirs: number;
|
||||
unread_total: number;
|
||||
/** couple_packs carries no timestamp, so equal-and-opposite toggles are caught by a digest. */
|
||||
enabled_packs_fingerprint: string | null;
|
||||
/** Digest of each visible match's type and response summary; moves on an in-place response edit that the count/latest-at markers miss. */
|
||||
match_state_fingerprint: string;
|
||||
/** Digest of each visible match's unread count; moves when unread redistributes between matches without changing the total. */
|
||||
match_unread_fingerprint: string;
|
||||
/** couple_streaks.updated_at, so a partner's answer can silently refresh an already-loaded streak. */
|
||||
streak_updated_at: string | null;
|
||||
}
|
||||
|
||||
export const appDataApi = {
|
||||
syncSummary: () => apiClient.get<SyncSummary>('/v1/me/sync'),
|
||||
packContext: (packId: string) => apiClient.get<{ pack: { id: string; name: string; icon: string | null } }>(`/v1/packs/${encodeURIComponent(packId)}/context`),
|
||||
packQuestions: (packId: string) => apiClient.get<{ questions: Question[] }>(`/v1/packs/${encodeURIComponent(packId)}/questions`),
|
||||
packTeaser: (packId: string) => apiClient.get<{ questions: Array<{ id: string; text: string; intensity: number }> }>(`/v1/packs/${encodeURIComponent(packId)}/teaser`),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { AppState, type AppStateStatus } from 'react-native';
|
||||
|
||||
/**
|
||||
* `background` is a real suspension and `inactive` is the moment behind the app
|
||||
* switcher, a call, or a system prompt. Anything else — including states a test
|
||||
* harness never sets — counts as foreground, so recurring work fails towards
|
||||
* running rather than towards being silently disabled forever.
|
||||
*/
|
||||
export function isForegroundState(state: AppStateStatus | string | undefined): boolean {
|
||||
return state !== 'background' && state !== 'inactive';
|
||||
}
|
||||
|
||||
export function isForeground(): boolean {
|
||||
return isForegroundState(AppState.currentState as AppStateStatus | undefined);
|
||||
}
|
||||
|
||||
/** Reports the new foreground flag on every transition. Returns an unsubscribe. */
|
||||
export function subscribeToForeground(onChange: (foreground: boolean) => void): () => void {
|
||||
const subscription = AppState.addEventListener('change', (state) => onChange(isForegroundState(state)));
|
||||
return () => subscription.remove();
|
||||
}
|
||||
@@ -1,8 +1,40 @@
|
||||
import { apiClient } from './apiClient';
|
||||
import type { Message, ReportReason } from '../features/chat/types';
|
||||
|
||||
export interface PartnerTyping {
|
||||
typing: boolean;
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A snapshot when `complete`, otherwise only what changed since the cursor the
|
||||
* previous page handed back. Delta rows are whole messages, not patches, so a
|
||||
* caller merges them by id without a second shape to understand.
|
||||
*/
|
||||
export interface MessagesPage {
|
||||
messages: Message[];
|
||||
/** Messages the caller deleted for themselves since the cursor. */
|
||||
removed_ids?: string[];
|
||||
/** Cursor to send as `since` on the next poll. */
|
||||
server_time?: string;
|
||||
complete?: boolean;
|
||||
/** Present when requested, saving a separate typing request. */
|
||||
typing?: PartnerTyping;
|
||||
}
|
||||
|
||||
export interface ListMessagesOptions {
|
||||
since?: string;
|
||||
typing?: boolean;
|
||||
}
|
||||
|
||||
export const chatApi = {
|
||||
listMessages: (matchId: string) => apiClient.get<{ messages: Message[] }>(`/v1/matches/${encodeURIComponent(matchId)}/messages`),
|
||||
listMessages: (matchId: string, options: ListMessagesOptions = {}) => {
|
||||
const query: string[] = [];
|
||||
if (options.since) query.push(`since=${encodeURIComponent(options.since)}`);
|
||||
if (options.typing) query.push('typing=true');
|
||||
const suffix = query.length > 0 ? `?${query.join('&')}` : '';
|
||||
return apiClient.get<MessagesPage>(`/v1/matches/${encodeURIComponent(matchId)}/messages${suffix}`);
|
||||
},
|
||||
sendText: (matchId: string, content: string) => apiClient.post<{ message: Message }>(`/v1/matches/${encodeURIComponent(matchId)}/messages`, { content }),
|
||||
unread: () => apiClient.get<{ total: number; by_match: Record<string, number> }>('/v1/chat/unread'),
|
||||
markRead: (matchId: string) => apiClient.post<{ updated: number; read_at: string }>(`/v1/matches/${encodeURIComponent(matchId)}/read`),
|
||||
@@ -11,5 +43,5 @@ export const chatApi = {
|
||||
deleteForEveryone: (messageId: string) => apiClient.delete<{ deleted: true; message: Message }>(`/v1/messages/${encodeURIComponent(messageId)}?scope=everyone`),
|
||||
report: (messageId: string, reason: ReportReason) => apiClient.post<{ reported: true }>(`/v1/messages/${encodeURIComponent(messageId)}/reports`, { reason }),
|
||||
setTyping: (matchId: string) => apiClient.put<{ typing: true }>(`/v1/matches/${encodeURIComponent(matchId)}/typing`),
|
||||
getTyping: (matchId: string) => apiClient.get<{ typing: boolean; expires_at: string | null }>(`/v1/matches/${encodeURIComponent(matchId)}/typing`),
|
||||
getTyping: (matchId: string) => apiClient.get<PartnerTyping>(`/v1/matches/${encodeURIComponent(matchId)}/typing`),
|
||||
};
|
||||
Reference in new issue
Block a user