Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/services/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,11 @@ export const apiService = {
)
);
},
post: (url: string, data: any) => apiClient.post(url, data),
put: (url: string, data: any) => apiClient.put(url, data),
delete: (url: string) => apiClient.delete(url),
/** #801: Generic response type so callers can infer the response shape. */
post: <T = unknown>(url: string, data: unknown) => apiClient.post<T>(url, data),
put: <T = unknown>(url: string, data: unknown) => apiClient.put<T>(url, data),
patch: <T = unknown>(url: string, data: unknown) => apiClient.patch<T>(url, data),
delete: <T = unknown>(url: string) => apiClient.delete<T>(url),
};

export { default as apiClient } from './axios.config';
Expand Down
8 changes: 5 additions & 3 deletions src/services/api/requestQueue.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AsyncStorage from '@react-native-async-storage/async-storage';
import type { ApiClient } from '../../types/apiClient';
import { InternalAxiosRequestConfig } from 'axios';
import * as Network from 'expo-network';

Expand Down Expand Up @@ -64,7 +65,8 @@ class RequestQueue {
private listeners: ((count: number) => void)[] = [];
private monitoringInterval: ReturnType<typeof setInterval> | null = null;
private networkListener: (() => void) | null = null;
private apiClient: any | null = null;
// #802: typed as ApiClient to prevent passing arbitrary objects
private apiClient: ApiClient | null = null;
private metrics: QueueMetrics = {
totalQueued: 0,
byPriority: { critical: 0, high: 0, normal: 0, low: 0 },
Expand Down Expand Up @@ -202,7 +204,7 @@ class RequestQueue {
}
}

async processQueue(apiClient?: any): Promise<void> {
async processQueue(apiClient?: ApiClient): Promise<void> {
if (this.isProcessing) return;

if (apiClient) {
Expand Down Expand Up @@ -283,7 +285,7 @@ class RequestQueue {
.catch(() => {});
}

startMonitoring(apiClient?: any): void {
startMonitoring(apiClient?: ApiClient): void {
if (apiClient) {
this.apiClient = apiClient;
}
Expand Down
8 changes: 5 additions & 3 deletions src/store/createStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ export const createStore = <T extends object>(
storage: createJSONStorage(() => asyncMMKVStorage),
partialize,
migrate,
onRehydrateStorage: (state) => {
return (state, error) => {
onRehydrateStorage: (_initialState) => {
// #803: explicitly type both params so implicit `any` is eliminated.
// `error` is `Error | undefined` per the Zustand persist middleware signature.
return (_rehydratedState: T | undefined, error: Error | undefined) => {
if (error) {
appLogger.errorSync('an error happened during hydration', error instanceof Error ? error : new Error(String(error)))
logger.errorSync('an error happened during hydration', error as Error)
}
}
};
},
}),
{ name: `Teach-This-${name}` }
Expand Down
20 changes: 20 additions & 0 deletions src/types/apiClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// src/types/apiClient.ts
// Typed interface that RequestQueue uses to replay queued requests.
// The exported `apiClient` from axios.config.ts satisfies this interface.

import type { AxiosResponse, InternalAxiosRequestConfig } from 'axios';

/**
* Minimum contract required by RequestQueue to execute queued HTTP requests.
*
* Typed as a callable (for replaying raw configs) plus named method overloads
* so TypeScript can verify the actual apiClient satisfies it at compile time.
*/
export interface ApiClient {
(config: InternalAxiosRequestConfig): Promise<AxiosResponse>;
get<T = unknown>(url: string, config?: object): Promise<AxiosResponse<T>>;
post<T = unknown>(url: string, data?: unknown, config?: object): Promise<AxiosResponse<T>>;
put<T = unknown>(url: string, data?: unknown, config?: object): Promise<AxiosResponse<T>>;
patch<T = unknown>(url: string, data?: unknown, config?: object): Promise<AxiosResponse<T>>;
delete<T = unknown>(url: string, config?: object): Promise<AxiosResponse<T>>;
}
33 changes: 33 additions & 0 deletions src/types/axios.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// src/types/axios.d.ts
// Module augmentation for custom Axios request config properties.
// This is the single source of truth — no more `as any` casts needed.

import 'axios';

declare module 'axios' {
interface InternalAxiosRequestConfig {
/**
* Set to `true` once a 401 has triggered a token-refresh attempt for this
* request, preventing infinite refresh loops.
*/
_retry?: boolean;

/**
* Running count of retry attempts for 429 (rate-limit) and 5xx (server
* error) responses. Starts at 0 and increments before each retry.
*/
_retryCount?: number;

/**
* Epoch milliseconds recorded when the request was dispatched. Used to
* calculate response latency in the response interceptor.
*/
_requestStartMs?: number;

/**
* Performance timing finish callback injected by the request interceptor.
* Set to `undefined` after first use to avoid double-reporting on retries.
*/
_timingFinish?: ((success: boolean, statusCode?: number) => unknown) | undefined;
}
}
Loading