Просмотр исходного кода

nmDb refresh + fallback to api

Viktoriia 3 недель назад
Родитель
Сommit
776c910d8d

+ 74 - 47
src/db/index.ts

@@ -9,6 +9,14 @@ import NetInfo from '@react-native-community/netinfo';
 let db1: SQLite.SQLiteDatabase | null = null;
 let db2: SQLite.SQLiteDatabase | null = null;
 let db3: SQLite.SQLiteDatabase | null = null;
+
+type DbKey = 'nm' | 'darePlaces' | 'countries';
+
+const refreshState: Record<DbKey, { isRefreshing: boolean; promise: Promise<void> | null }> = {
+  nm: { isRefreshing: false, promise: null },
+  darePlaces: { isRefreshing: false, promise: null },
+  countries: { isRefreshing: false, promise: null }
+};
 const nmRegionsDBname = 'nmRegions.db';
 const darePlacesDBname = 'darePlaces.db';
 const countriesDBname = 'nmCountries.db';
@@ -16,6 +24,24 @@ const sqliteDirectory = 'SQLite';
 const sqliteFullPath = FileSystem.documentDirectory + sqliteDirectory;
 const DS = '/';
 
+export function isNmDbRefreshing(): boolean {
+  return refreshState.nm.isRefreshing;
+}
+
+function runRefresh(key: DbKey, fn: () => Promise<void>): Promise<void> {
+  const state = refreshState[key];
+  if (state.isRefreshing && state.promise) {
+    console.log(`[DB] "${key}" already refreshing, reusing promise`);
+    return state.promise;
+  }
+  state.isRefreshing = true;
+  state.promise = fn().finally(() => {
+    state.isRefreshing = false;
+    state.promise = null;
+  });
+  return state.promise;
+}
+
 async function copyDatabaseFile(dbName: string, dbAsset: Asset) {
   const state = await NetInfo.fetch();
 
@@ -166,66 +192,67 @@ const openDatabase = (dbName: string) =>
     useNewConnection: true
   });
 
-async function refreshNmDatabase() {
-  try {
-    await FileSystem.deleteAsync(sqliteFullPath + DS + nmRegionsDBname, { idempotent: true });
-    const nmUrl = `${API_HOST}/static/app/${nmRegionsDBname}`;
-    let nmFileUri = sqliteFullPath + DS + nmRegionsDBname;
+export function refreshNmDatabase(): Promise<void> {
+  return runRefresh('nm', async () => {
+    try {
+      await FileSystem.deleteAsync(sqliteFullPath + DS + nmRegionsDBname, { idempotent: true });
+      const nmUrl = `${API_HOST}/static/app/${nmRegionsDBname}`;
+      const nmFileUri = sqliteFullPath + DS + nmRegionsDBname;
+      const nmResponse = await FileSystem.downloadAsync(nmUrl, nmFileUri);
 
-    const nmResponse = await FileSystem.downloadAsync(nmUrl, nmFileUri);
+      if (nmResponse.status !== 200) {
+        console.error(`Failed to download nmDb: Status ${nmResponse.status}`);
+      }
 
-    if (nmResponse.status !== 200) {
-      console.error(`Failed to download the nmDb file: Status code ${nmResponse.status}`);
+      db1 = null;
+      db1 = openDatabase(nmRegionsDBname);
+    } catch (error) {
+      console.error('refreshNmDatabase - Error:');
+      console.error(JSON.stringify(error, null, 2));
     }
-
-    db1 = null;
-    db1 = openDatabase(nmRegionsDBname);
-  } catch (error) {
-    console.error('refreshDatabase nmRegions - Error:');
-    console.error(JSON.stringify(error, null, 2));
-  }
+  });
 }
 
-async function refreshDarePlacesDatabase() {
-  try {
-    await FileSystem.deleteAsync(sqliteFullPath + DS + darePlacesDBname, { idempotent: true });
-    const dareUrl = `${API_HOST}/static/app/${darePlacesDBname}`;
-    let dareFileUri = sqliteFullPath + DS + darePlacesDBname;
+export function refreshDarePlacesDatabase(): Promise<void> {
+  return runRefresh('darePlaces', async () => {
+    try {
+      await FileSystem.deleteAsync(sqliteFullPath + DS + darePlacesDBname, { idempotent: true });
+      const dareUrl = `${API_HOST}/static/app/${darePlacesDBname}`;
+      const dareFileUri = sqliteFullPath + DS + darePlacesDBname;
+      const dareResponse = await FileSystem.downloadAsync(dareUrl, dareFileUri);
 
-    const dareResponse = await FileSystem.downloadAsync(dareUrl, dareFileUri);
+      if (dareResponse.status !== 200) {
+        console.error(`Failed to download dareDb: Status ${dareResponse.status}`);
+      }
 
-    if (dareResponse.status !== 200) {
-      console.error(`Failed to download the dareDb file: Status code ${dareResponse.status}`);
+      db2 = null;
+      db2 = openDatabase(darePlacesDBname);
+    } catch (error) {
+      console.error('refreshDarePlacesDatabase - Error:');
+      console.error(JSON.stringify(error, null, 2));
     }
-
-    db2 = null;
-    db2 = openDatabase(darePlacesDBname);
-  } catch (error) {
-    console.error('refreshDatabase darePlaces - Error:');
-    console.error(JSON.stringify(error, null, 2));
-  }
+  });
 }
 
-async function refreshCountriesDatabase() {
-  try {
-    await FileSystem.deleteAsync(sqliteFullPath + DS + countriesDBname, { idempotent: true });
-    const countriesUrl = `${API_HOST}/static/app/${countriesDBname}`;
-    let countriesFileUri = sqliteFullPath + DS + countriesDBname;
+export function refreshCountriesDatabase(): Promise<void> {
+  return runRefresh('countries', async () => {
+    try {
+      await FileSystem.deleteAsync(sqliteFullPath + DS + countriesDBname, { idempotent: true });
+      const countriesUrl = `${API_HOST}/static/app/${countriesDBname}`;
+      const countriesFileUri = sqliteFullPath + DS + countriesDBname;
+      const countriesResponse = await FileSystem.downloadAsync(countriesUrl, countriesFileUri);
 
-    const countriesResponse = await FileSystem.downloadAsync(countriesUrl, countriesFileUri);
+      if (countriesResponse.status !== 200) {
+        console.error(`Failed to download countriesDb: Status ${countriesResponse.status}`);
+      }
 
-    if (countriesResponse.status !== 200) {
-      console.error(
-        `Failed to download the countriesDb file: Status code ${countriesResponse.status}`
-      );
+      db3 = null;
+      db3 = openDatabase(countriesDBname);
+    } catch (error) {
+      console.error('refreshCountriesDatabase - Error:');
+      console.error(JSON.stringify(error, null, 2));
     }
-
-    db3 = null;
-    db3 = openDatabase(countriesDBname);
-  } catch (error) {
-    console.error('refreshDatabase nmCountries - Error:');
-    console.error(JSON.stringify(error, null, 2));
-  }
+  });
 }
 
 export async function updateNmRegionsDb(localLastDate: string) {

+ 1 - 0
src/modules/api/regions/queries/index.ts

@@ -7,3 +7,4 @@ export * from './use-post-get-users-from-region';
 export * from './use-post-get-users-who-visited-region';
 export * from './use-post-get-users-who-visited-dare';
 export * from './use-get-list-regions';
+export * from './use-post-get-map-data';

+ 23 - 0
src/modules/api/regions/queries/use-post-get-map-data.tsx

@@ -0,0 +1,23 @@
+import { regionQueryKeys } from '../regions-query-keys';
+import { regionsApi, type PostGetMapDataReturn } from '../regions-api';
+import { queryClient } from 'src/utils/queryClient';
+
+export const fetchMapData = async (id: number) => {
+  try {
+    const data: PostGetMapDataReturn = await queryClient.fetchQuery({
+      queryKey: regionQueryKeys.getMapData(),
+      queryFn: async () => {
+        const response = await regionsApi.getMapData(
+          id,
+        )
+        return response.data;
+      },
+      gcTime: 0,
+      staleTime: 0
+    });
+
+    return data;
+  } catch (error) {
+    console.error('Failed to fetch map data:', error);
+  }
+};

+ 13 - 1
src/modules/api/regions/regions-api.tsx

@@ -143,6 +143,17 @@ export interface PostGetListRegionsReturn extends ResponseType {
   }[];
 }
 
+export interface PostGetMapDataReturn extends ResponseType {
+  data: {
+    avatars: { data: string; id: number }[];
+    id: number;
+    name: string;
+    region_photos: string[];
+    visitors_avatars: number[];
+    visitors_count: number;
+  };
+}
+
 export const regionsApi = {
   getRegionsWithFlag: () => request.postForm<PostGetRegionsReturn>(API.GET_REGIONS_WITH_FLAGS),
   getUserData: (region_id: number, token: string) =>
@@ -188,5 +199,6 @@ export const regionsApi = {
       age,
       country
     }),
-  getListRegions: () => request.postForm<PostGetListRegionsReturn>(API.GET_LIST_REGIONS)
+  getListRegions: () => request.postForm<PostGetListRegionsReturn>(API.GET_LIST_REGIONS),
+  getMapData: (id: number) => request.postForm<PostGetMapDataReturn>(API.GET_MAP_DATA, { id })
 };

+ 2 - 1
src/modules/api/regions/regions-query-keys.tsx

@@ -7,5 +7,6 @@ export const regionQueryKeys = {
   getUsersFromRegion: () => ['getUsersFromRegion'] as const,
   getUsersWhoVisitedRegion: () => ['getUsersWhoVisitedRegion'] as const,
   getUsersWhoVisitedDare: () => ['getUsersWhoVisitedDare'] as const,
-  getListRegions: () => ['getListRegions'] as const
+  getListRegions: () => ['getListRegions'] as const,
+  getMapData: () => ['getMapData'] as const,
 };

+ 53 - 2
src/modules/map/regionData.ts

@@ -1,4 +1,8 @@
+import { fetchMapData } from '@api/regions';
 import { SQLiteDatabase } from 'expo-sqlite';
+import { isNmDbRefreshing, refreshNmDatabase } from 'src/db';
+
+const NM_TABLES = new Set(['regions']);
 
 export const getData = async (
   db: SQLiteDatabase | null,
@@ -6,12 +10,36 @@ export const getData = async (
   name: string,
   callback: (data: any, avatars: string[]) => void
 ): Promise<void> => {
-  if (!db) throw new Error('Database is null');
+  const isNmTable = NM_TABLES.has(name);
+
+  if (isNmTable && isNmDbRefreshing()) {
+    console.warn(`[getData] nm DB is refreshing, falling back to API for "${name}"`);
+    await fallbackToApi(regionId, callback);
+    return;
+  }
+
+  if (!db) {
+    if (isNmTable) {
+      console.warn(`[getData] db is null for "${name}", falling back to API`);
+      refreshNmDatabase();
+      await fallbackToApi(regionId, callback);
+      return;
+    }
+    throw new Error(`[getData] Database is null for table="${name}"`);
+  }
 
   try {
     const regionRows = await db.getAllAsync<any>(`SELECT * FROM ${name} WHERE id = ?;`, [regionId]);
 
     const regionData = regionRows[0] ?? null;
+    if (!regionData && isNmTable) {
+      console.warn(
+        `[getData] No record found for id=${regionId} in "${name}", falling back to API`
+      );
+      refreshNmDatabase();
+      await fallbackToApi(regionId, callback);
+      return;
+    }
 
     const avatarIds: number[] = regionData?.visitors_avatars
       ? JSON.parse(regionData.visitors_avatars)
@@ -30,6 +58,29 @@ export const getData = async (
     setTimeout(() => callback(regionData, avatars), 0);
   } catch (error) {
     console.error('Error in getData:', error);
-    throw error;
+
+    if (isNmTable) {
+      refreshNmDatabase();
+      await fallbackToApi(regionId, callback);
+    } else {
+      throw error;
+    }
   }
 };
+
+async function fallbackToApi(
+  regionId: number,
+  callback: (data: any, avatars: string[]) => void
+): Promise<void> {
+  try {
+    const response = await fetchMapData(regionId);
+    const regionData = response?.data ?? null;
+    const avatars: string[] =
+      regionData?.avatars?.slice(0, 3).map((a: { data: string; id: number }) => a.data) ?? [];
+
+    setTimeout(() => callback(regionData, avatars), 0);
+  } catch (apiError) {
+    console.error('[getData] API fallback failed:', apiError);
+    throw apiError;
+  }
+}

+ 4 - 2
src/types/api.ts

@@ -224,7 +224,8 @@ export enum API_ENDPOINT {
   GET_CONVERSATION_WITH_ALL = 'get-conversation-with-all',
   GET_USERS_WHO_TICKED_SERIES = 'get-users-who-ticked-series',
   GET_TRIPS_FOR_REGION = 'get-trips-for-region',
-  GET_REGIONS_THAT_HAVE_TRIPS = 'get-regions-that-have-trips'
+  GET_REGIONS_THAT_HAVE_TRIPS = 'get-regions-that-have-trips',
+  GET_MAP_DATA = 'get-map-data'
 }
 
 export enum API {
@@ -422,7 +423,8 @@ export enum API {
   GET_CONVERSATION_WITH_ALL = `${API_ROUTE.CHAT}/${API_ENDPOINT.GET_CONVERSATION_WITH_ALL}`,
   GET_USERS_WHO_TICKED_SERIES = `${API_ROUTE.SERIES}/${API_ENDPOINT.GET_USERS_WHO_TICKED_SERIES}`,
   GET_TRIPS_FOR_REGION = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_TRIPS_FOR_REGION}`,
-  GET_REGIONS_THAT_HAVE_TRIPS = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_REGIONS_THAT_HAVE_TRIPS}`
+  GET_REGIONS_THAT_HAVE_TRIPS = `${API_ROUTE.TRIPS}/${API_ENDPOINT.GET_REGIONS_THAT_HAVE_TRIPS}`,
+  GET_MAP_DATA = `${API_ROUTE.REGIONS}/${API_ENDPOINT.GET_MAP_DATA}`
 }
 
 export type BaseAxiosError = AxiosError;