Skip to content

Commit

Permalink
Offline Caching fix!
Browse files Browse the repository at this point in the history
  • Loading branch information
thedev132 committed Feb 1, 2025
1 parent 52d8469 commit d501eb4
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 52 deletions.
12 changes: 3 additions & 9 deletions App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import * as SecureStorage from "expo-secure-store";
import { useState, useEffect, useCallback } from "react";
import { StatusBar, useColorScheme } from "react-native";
import { SafeAreaProvider } from "react-native-safe-area-context";
import { SWRConfig } from "swr";

import AuthContext from "./src/auth";
import asyncStorageProvider from "./src/cacheProvider";
import { SWRWrapper } from "./src/components/SWRWrapper";
import { getStateFromPath } from "./src/getStateFromPath";
import useClient from "./src/lib/client";
import { TabParamList } from "./src/lib/NavigatorParamList";
Expand Down Expand Up @@ -112,12 +111,7 @@ export default function App() {
backgroundColor={palette.background}
/>

<SWRConfig
value={{
provider: asyncStorageProvider,
fetcher,
}}
>
<SWRWrapper fetcher={fetcher}>
<SafeAreaProvider>
<ActionSheetProvider>
<NavigationContainer
Expand All @@ -128,7 +122,7 @@ export default function App() {
</NavigationContainer>
</ActionSheetProvider>
</SafeAreaProvider>
</SWRConfig>
</SWRWrapper>
</AuthContext.Provider>
);
}
17 changes: 9 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/react-native-fontawesome": "^0.3.2",
"@hackclub/hackclub-icons-rn": "github:thedev132/hackclub-icons-rn",
"@react-native-async-storage/async-storage": "1.23.1",
"@react-native-community/geolocation": "^3.3.0",
"@react-native-menu/menu": "^1.1.6",
Expand Down Expand Up @@ -54,7 +55,6 @@
"expo-splash-screen": "~0.29.18",
"expo-status-bar": "~2.0.0",
"expo-system-ui": "~4.0.6",
"expo-updates": "^0.26.13",
"expo-web-browser": "~14.0.1",
"geopattern": "github:thedev132/geopattern",
"humanize-string": "^3.0.0",
Expand Down
34 changes: 0 additions & 34 deletions src/cacheProvider.ts

This file was deleted.

134 changes: 134 additions & 0 deletions src/components/SWRWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import * as FileSystem from "expo-file-system";
import { useEffect, useMemo, useRef, useState, ReactNode } from "react";
import {
AppState,
AppStateStatus,
InteractionManager,
Platform,
} from "react-native";
import "react-native-gesture-handler";
import { SWRConfig } from "swr";

async function fileSystemProvider(cacheData: Map<unknown, unknown> | null) {
InteractionManager.runAfterInteractions(async () => {
const cacheDir = FileSystem.cacheDirectory + "hcb-mobile-cache/";
const file = `${cacheDir}cache.json`;

async function ensureDirExists() {
const dirInfo = await FileSystem.getInfoAsync(cacheDir);
if (!dirInfo.exists) {
console.log("Cache directory doesn't exist, creating…");
await FileSystem.makeDirectoryAsync(cacheDir, { intermediates: true });
}
}

const map = cacheData || new Map();

if (map.size === 0) {
console.log("⛔ Cache is empty, not saving to disk.");
return;
}
// console.log("💾 Saving cache to disk…");
await ensureDirExists();
await FileSystem.writeAsStringAsync(
file,
JSON.stringify(Array.from(map.entries())),
);
// console.log(`💾 Saved ${map.size} API routes to cache! `);
});
}

interface SWRWrapperProps {
fetcher: (url: string, options: RequestInit) => Promise<unknown>;
children: ReactNode;
}

export function SWRWrapper({ fetcher, children }: SWRWrapperProps) {
const cacheData = useRef(new Map());
const lastSaveTime = useRef(Date.now());
const [cacheLoaded, setCacheLoaded] = useState(Platform.OS === "web");

// Effect to handle periodic cache saving
useEffect(() => {
const interval = setInterval(() => {
if (cacheData.current && Date.now() - lastSaveTime.current >= 10000) {
fileSystemProvider(cacheData.current);
lastSaveTime.current = Date.now();
}
}, 10000);

return () => clearInterval(interval);
}, []);

// Effect to handle app state changes
useEffect(() => {
const save = () => {
if (cacheData.current) {
fileSystemProvider(cacheData.current);
lastSaveTime.current = Date.now();
}
};

const subscription = AppState.addEventListener("change", save);

return () => {
subscription.remove();
};
}, []);

// Effect to load initial cache
useEffect(() => {
(async () => {
if (cacheLoaded) return;
const cacheDir = FileSystem.cacheDirectory + "hcb-mobile-cache/";
const file = `${cacheDir}cache.json`;
const fileInfo = await FileSystem.getInfoAsync(file);
if (fileInfo.exists) {
console.log("📂 Cache file exists, restoring cache…");
const data = await FileSystem.readAsStringAsync(file);
const entries = JSON.parse(data);
cacheData.current = new Map(entries);
console.log(`📂 Restored ${cacheData.current.size} API routes!`);
} else {
console.log("📂 Cache file doesn't exist, creating new cache…");
cacheData.current = new Map();
}
if (!cacheLoaded) setCacheLoaded(true);
})();
}, [cacheData, cacheLoaded]);

const contextValue = useMemo(
() => ({
provider: () => cacheData.current,
isVisible: () => true,
fetcher,
initFocus(callback: () => void) {
let appState: AppStateStatus = AppState.currentState as AppStateStatus;

const onAppStateChange = (nextAppState: string) => {
if (
appState.match(/inactive|background/) &&
nextAppState === "active"
) {
callback();
}
appState = nextAppState as AppStateStatus;
};

const subscription = AppState.addEventListener(
"change",
onAppStateChange,
);

return () => {
subscription.remove();
};
},
}),
[fetcher],
);

return cacheLoaded ? (
<SWRConfig value={contextValue}>{children}</SWRConfig>
) : null;
}

0 comments on commit d501eb4

Please sign in to comment.