catalyst-core 0.2.0-beta.1 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/native/androidProject/app/build.gradle.kts +25 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +239 -75
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +3 -2
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/OfflineCacheService.kt +374 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/WebCacheManager.kt +136 -26
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
- package/dist/native/buildAndroid/assets.js +2 -2
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +39 -4
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineCacheService.swift +683 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/OfflineURLSchemeHandler.swift +94 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/ResourceURLProtocol.swift +19 -6
- package/dist/native/iosnativeWebView/Sources/Core/Utils/SafeAreaUtils.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeCommandHandler.swift +1 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +15 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +82 -2
- package/dist/native/renameAndroidProject.js +2 -2
- package/dist/offline/catalyst-sw.js +1 -0
- package/dist/offline/registerServiceWorker.js +1 -0
- package/dist/router/ClientRouter.js +1 -1
- package/dist/scripts/build.js +1 -1
- package/dist/scripts/devBuild.js +1 -1
- package/dist/scripts/generateOfflineManifest.js +1 -0
- package/dist/server/expressServer.js +2 -1
- package/dist/server/renderer/document/Body.js +1 -1
- package/dist/server/renderer/handler.js +3 -3
- package/dist/webpack/base.babel.js +1 -1
- package/mcp_v2/knowledge-base.json +13 -0
- package/package.json +1 -1
package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt
CHANGED
|
@@ -85,7 +85,7 @@ class CustomWebviewTest {
|
|
|
85
85
|
setProperty("accessControl.allowedUrls", "https://example.com,https://api.example.com")
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
val enabled = properties.getProperty("accessControl.enabled", "
|
|
88
|
+
val enabled = properties.getProperty("accessControl.enabled", "false").equals("true", ignoreCase = true)
|
|
89
89
|
assertTrue("Access control should be enabled", enabled)
|
|
90
90
|
|
|
91
91
|
val allowedUrls = properties.getProperty("accessControl.allowedUrls", "")
|
|
@@ -101,8 +101,13 @@ class CustomWebviewTest {
|
|
|
101
101
|
val disabledProps = Properties().apply {
|
|
102
102
|
setProperty("accessControl.enabled", "false")
|
|
103
103
|
}
|
|
104
|
-
val disabled = disabledProps.getProperty("accessControl.enabled", "
|
|
104
|
+
val disabled = disabledProps.getProperty("accessControl.enabled", "false").equals("true", ignoreCase = true)
|
|
105
105
|
assertFalse("Access control should be disabled", disabled)
|
|
106
|
+
|
|
107
|
+
// Test missing config defaults to disabled
|
|
108
|
+
val missingProps = Properties()
|
|
109
|
+
val missing = missingProps.getProperty("accessControl.enabled", "false").equals("true", ignoreCase = true)
|
|
110
|
+
assertFalse("Missing access control config should default to disabled", missing)
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
@Test
|
|
@@ -7,7 +7,7 @@ const source=findIconForDensity(density.key);cleanDensityDir(targetDir);if(sourc
|
|
|
7
7
|
fs.copyFileSync(source.path,destination);hasCustomIcons=true;}}if(!hasCustomIcons&&fallbackExists){const targetDir=path.join(destPath,"mipmap-xxxhdpi");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - destPath is derived from pwd (internal); "mipmap-xxxhdpi" is a fixed literal.
|
|
8
8
|
fs.mkdirSync(targetDir,{recursive:true});cleanDensityDir(targetDir);const destination=path.join(targetDir,"icon.png");// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - targetDir is derived from pwd (internal); "icon.png" is a fixed literal.
|
|
9
9
|
fs.copyFileSync(fallbackIconPath,destination);usedFallback=true;}const setManifestIcons=(iconValue,roundIconValue)=>{let current;try{current=fs.readFileSync(manifestPath,"utf8");}catch(e){if(e.code==="ENOENT")return;throw e;}const updated=current.replace(/android:icon="[^"]*"/g,`android:icon="${iconValue}"`).replace(/android:roundIcon="[^"]*"/g,`android:roundIcon="${roundIconValue}"`);if(updated!==current)fs.writeFileSync(manifestPath,updated);};if(!hasCustomIcons&&!usedFallback){progress.log("No custom Android icons found; using default template icons.","info");setManifestIcons("@mipmap/ic_launcher","@mipmap/ic_launcher_round");return;}setManifestIcons("@mipmap/icon","@mipmap/icon");if(hasCustomIcons){progress.log("Applied Android launcher icons from public/android/appIcons.","success");}if(usedFallback){progress.log("Used bundled Catalyst fallback icon for launcher.","info");}}catch(error){progress.log(`Warning: Error copying app icon assets: ${error.message}`,"warning");}}async function configureAppName(androidConfig){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const stringsFile=`${destPath}/values/strings.xml`;let stringsContent=fs.readFileSync(stringsFile,"utf8");if(androidConfig.appName){stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">${androidConfig.appName}</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log(`App display name configured: ${androidConfig.appName}`,"success");}else{stringsContent=stringsContent.replace(/<string name="app_name">.*?<\/string>/,`<string name="app_name">Catalyst Application</string>`);fs.writeFileSync(stringsFile,stringsContent);progress.log("App display name reverted to default: Catalyst Application","info");}}catch(error){progress.log(`Warning: Error configuring app name: ${error.message}`,"warning");}}// ── Notification helpers ──────────────────────────────────────────────────
|
|
10
|
-
async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.cwd()}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;if(fs.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");const appDir=`${pwd}/androidProject/app`;if(!fs.existsSync(appDir)){fs.mkdirSync(appDir,{recursive:true});}fs.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(fs.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
10
|
+
async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${process.cwd()}/google-services.json`;const androidGoogleServicesPath=`${pwd}/androidProject/app/google-services.json`;if(fs.existsSync(rootGoogleServicesPath)){progress.log("Found google-services.json in root directory","info");const appDir=`${pwd}/androidProject/app`;if(!fs.existsSync(appDir)){fs.mkdirSync(appDir,{recursive:true});}fs.copyFileSync(rootGoogleServicesPath,androidGoogleServicesPath);progress.log("Copied google-services.json to androidProject/app/","success");return true;}else if(fs.existsSync(androidGoogleServicesPath)){progress.log("google-services.json already exists in androidProject/app/","info");return true;}else{progress.log("google-services.json not found - Firebase push notifications will not work","warning");progress.log("Place google-services.json in project root or src/native/androidProject/app/","info");return false;}}catch(error){progress.log(`Warning: Error handling google-services.json: ${error.message}`,"warning");return false;}}async function processNotifications(WEBVIEW_CONFIG){const hasNotificationConfig=!!WEBVIEW_CONFIG.notifications?.enabled;try{await writeGeneratedBuildProperties(hasNotificationConfig);await cleanupNotificationPermissions();await cleanupNotificationResources();await cleanupNotificationMetadata();await cleanupNotificationAssets();if(!hasNotificationConfig){progress.log("Notifications disabled - cleaned up notification configurations","info");return;}const hasGoogleServices=await handleGoogleServicesJson();if(!hasGoogleServices){progress.log("Continuing without Firebase - only local notifications will work","warning");}await addNotificationPermissions();await generateNotificationResources(WEBVIEW_CONFIG.notifications);await addNotificationMetadata();await processNotificationAssets();progress.log("Notification configuration completed successfully!","success");}catch(error){progress.log(`Warning: Error processing notifications: ${error.message}`,"warning");}}async function writeGeneratedBuildProperties(notificationsEnabled){try{const propertiesPath=`${pwd}/androidProject/app/catalyst-build.properties`;const propertiesContent=["# Generated by Catalyst. Used by Android Studio builds when -PconfigPath is not provided.",`notifications.enabled=${notificationsEnabled?"true":"false"}`,""].join("\n");fs.writeFileSync(propertiesPath,propertiesContent);progress.log("Updated generated Android build properties","success");}catch(error){throw new Error(`Failed to write generated Android build properties: ${error.message}`);}}async function addNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const permissionsXml=` <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
11
11
|
<uses-permission android:name="android.permission.VIBRATE" />
|
|
12
12
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
|
13
13
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />`;manifestContent=manifestContent.replace(/(<uses-permission[^>]*>[\s\S]*?)(\s*<uses-feature)/,`$1\n${permissionsXml}$2`);fs.writeFileSync(manifestPath,manifestContent);progress.log("Added notification permissions to AndroidManifest.xml","success");}catch(error){throw new Error(`Failed to add notification permissions: ${error.message}`);}}async function generateNotificationResources(notificationConfig){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const notificationColorXml=` <color name="notification_color">${notificationConfig.color||"#007AFF"}</color>`;if(!colorsContent.includes('name="notification_color"')){colorsContent=colorsContent.replace(/(<\/resources>)/,`${notificationColorXml}\n$1`);fs.writeFileSync(colorsPath,colorsContent);progress.log("Added notification color to colors.xml","success");}}catch(error){throw new Error(`Failed to generate notification resources: ${error.message}`);}}async function addNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataXml=`
|
|
@@ -49,4 +49,4 @@ async function handleGoogleServicesJson(){try{const rootGoogleServicesPath=`${pr
|
|
|
49
49
|
<path
|
|
50
50
|
android:fillColor="@android:color/white"
|
|
51
51
|
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/>
|
|
52
|
-
</vector>`;fs.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];manifestContent=manifestContent.split("\n").filter(line=>{return!notificationPermissions.some(permission=>line.includes(`<uses-permission android:name="${permission}"`));}).join("\n");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");fs.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];
|
|
52
|
+
</vector>`;fs.writeFileSync(`${destPath}/drawable/${iconName}.xml`,iconXml);}async function cleanupNotificationPermissions(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const notificationPermissions=["android.permission.POST_NOTIFICATIONS","android.permission.VIBRATE","android.permission.WAKE_LOCK","android.permission.RECEIVE_BOOT_COMPLETED"];manifestContent=manifestContent.split("\n").filter(line=>{return!notificationPermissions.some(permission=>line.includes(`<uses-permission android:name="${permission}"`));}).join("\n");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification permissions: ${error.message}`,"warning");}}async function cleanupNotificationResources(){try{const colorsPath=`${pwd}/androidProject/app/src/main/res/values/colors.xml`;let colorsContent=fs.readFileSync(colorsPath,"utf8");const existingColorLine=colorsContent.split("\n").find(line=>line.includes('name="notification_color"'));if(existingColorLine){colorsContent=colorsContent.replace(existingColorLine,"");fs.writeFileSync(colorsPath,colorsContent);}}catch(error){progress.log(`Warning: Error cleaning notification resources: ${error.message}`,"warning");}}async function cleanupNotificationMetadata(){try{const manifestPath=`${pwd}/androidProject/app/src/main/AndroidManifest.xml`;let manifestContent=fs.readFileSync(manifestPath,"utf8");const metadataNames=["default_notification_channel_id","default_notification_icon","default_notification_color","com.google.firebase.messaging.default_notification_channel_id","com.google.firebase.messaging.default_notification_icon","com.google.firebase.messaging.default_notification_sound"];for(const metadataName of metadataNames){const metadataRegex=new RegExp(`\\s*<meta-data\\s+[\\s\\S]*?android:name="${metadataName}"[\\s\\S]*?\\/>`,"gi");manifestContent=manifestContent.replace(metadataRegex,"");}const serviceRegex=/\s*<!--\s*Push Notification Service\s*-->\s*<service[^>]*android:name="[^"]*PushNotificationUtils"[\s\S]*?<\/service>/gi;manifestContent=manifestContent.replace(serviceRegex,"");manifestContent=manifestContent.replace(/\s*<!--\s*Default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Firebase default notification configuration\s*-->/gi,"");manifestContent=manifestContent.replace(/\s*<!--\s*Push Notification Service\s*-->/gi,"");fs.writeFileSync(manifestPath,manifestContent);}catch(error){progress.log(`Warning: Error cleaning notification metadata: ${error.message}`,"warning");}}async function cleanupNotificationAssets(){try{const destPath=`${pwd}/androidProject/app/src/main/res`;const imageFormats=["png","jpg","jpeg","gif","bmp","svg","webp","xml"];const audioFormats=["mp3","wav","ogg"];const notificationIcons=["ic_notification","ic_notification_large"];const notificationSounds=["notification_sound_default","notification_sound_urgent"];for(const icon of notificationIcons){for(const format of imageFormats){const iconPath=`${destPath}/drawable/${icon}.${format}`;if(fs.existsSync(iconPath)){fs.unlinkSync(iconPath);}}}const rawDir=`${destPath}/raw`;if(fs.existsSync(rawDir)){for(const sound of notificationSounds){for(const format of audioFormats){const soundPath=`${rawDir}/${sound}.${format}`;if(fs.existsSync(soundPath)){fs.unlinkSync(soundPath);}}}}}catch(error){progress.log(`Warning: Error cleaning notification assets: ${error.message}`,"warning");}}return{copyBuildAssets,copySplashscreenAssets,copyOfflinePage,copyIconAssets,configureAppName,processNotifications};}module.exports=createAssetsPhase;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import Foundation
|
|
2
|
+
import CryptoKit
|
|
2
3
|
import os
|
|
3
4
|
|
|
4
5
|
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.app", category: "CacheManager")
|
|
@@ -118,8 +119,8 @@ public final class CacheManager {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
private func getCacheKey(for url: URL) -> String {
|
|
121
|
-
|
|
122
|
-
|
|
122
|
+
let digest = SHA256.hash(data: Data(url.absoluteString.utf8))
|
|
123
|
+
return digest.map { String(format: "%02x", $0) }.joined()
|
|
123
124
|
}
|
|
124
125
|
|
|
125
126
|
private func getCacheFilePath(for url: URL) -> URL {
|
|
@@ -140,6 +141,11 @@ public final class CacheManager {
|
|
|
140
141
|
|
|
141
142
|
return false
|
|
142
143
|
}
|
|
144
|
+
|
|
145
|
+
func shouldCacheRequest(_ request: URLRequest) -> Bool {
|
|
146
|
+
guard let url = request.url else { return false }
|
|
147
|
+
return shouldCacheURL(url) || OfflineCacheService.shared.shouldCacheOfflineRouteSubresource(request)
|
|
148
|
+
}
|
|
143
149
|
|
|
144
150
|
private func getCacheState(for timestamp: Date) -> CacheState {
|
|
145
151
|
let age = Date().timeIntervalSince(timestamp)
|
|
@@ -181,6 +187,10 @@ public final class CacheManager {
|
|
|
181
187
|
logger.info("🔴 Cache expired for: \(urlString)")
|
|
182
188
|
continuation.resume(returning: (nil, .expired, nil))
|
|
183
189
|
}
|
|
190
|
+
} else if let cachedResponse = manager.session.configuration.urlCache?.cachedResponse(for: request) {
|
|
191
|
+
let mimeType = (cachedResponse.response as? HTTPURLResponse)?.mimeType ?? "application/octet-stream"
|
|
192
|
+
logger.info("🟢 URLCache fallback hit for: \(urlString)")
|
|
193
|
+
continuation.resume(returning: (cachedResponse.data, .fresh, mimeType))
|
|
184
194
|
} else {
|
|
185
195
|
logger.info("❌ Cache miss for: \(urlString)")
|
|
186
196
|
continuation.resume(returning: (nil, .expired, nil))
|
|
@@ -193,10 +203,13 @@ public final class CacheManager {
|
|
|
193
203
|
logger.info("🔄 Starting revalidation for: \(request.url?.absoluteString ?? "")")
|
|
194
204
|
|
|
195
205
|
do {
|
|
196
|
-
let
|
|
206
|
+
let configuration = URLSessionConfiguration.ephemeral
|
|
207
|
+
configuration.protocolClasses = []
|
|
208
|
+
let networkSession = URLSession(configuration: configuration)
|
|
209
|
+
let (data, response) = try await networkSession.data(for: request)
|
|
197
210
|
|
|
198
211
|
if let httpResponse = response as? HTTPURLResponse,
|
|
199
|
-
isCacheableResponse(httpResponse) {
|
|
212
|
+
isCacheableResponse(httpResponse, for: request) {
|
|
200
213
|
storeCachedResponse(httpResponse, data: data, for: request)
|
|
201
214
|
logger.info("Resource revalidated: \(request.url?.absoluteString ?? "")")
|
|
202
215
|
}
|
|
@@ -263,6 +276,28 @@ public final class CacheManager {
|
|
|
263
276
|
guard let url = response.url else { return false }
|
|
264
277
|
return (200...299 ~= response.statusCode) && shouldCacheURL(url)
|
|
265
278
|
}
|
|
279
|
+
|
|
280
|
+
func isCacheableResponse(_ response: HTTPURLResponse, for request: URLRequest) -> Bool {
|
|
281
|
+
guard let url = response.url, 200...299 ~= response.statusCode else { return false }
|
|
282
|
+
|
|
283
|
+
let matchesLegacyPattern = shouldCacheURL(url)
|
|
284
|
+
let offlineRouteSubresource = OfflineCacheService.shared.shouldCacheOfflineRouteSubresource(request)
|
|
285
|
+
guard matchesLegacyPattern || offlineRouteSubresource else { return false }
|
|
286
|
+
|
|
287
|
+
if offlineRouteSubresource && !matchesLegacyPattern && isDocumentOrDataResponse(response) {
|
|
288
|
+
return false
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return true
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private func isDocumentOrDataResponse(_ response: HTTPURLResponse) -> Bool {
|
|
295
|
+
let mimeType = response.mimeType?.lowercased() ?? ""
|
|
296
|
+
return mimeType == "text/html" ||
|
|
297
|
+
mimeType == "application/json" ||
|
|
298
|
+
mimeType.hasSuffix("+json") ||
|
|
299
|
+
mimeType == "text/event-stream"
|
|
300
|
+
}
|
|
266
301
|
|
|
267
302
|
func getCacheStatistics() -> (memoryUsed: Int, diskUsed: Int) {
|
|
268
303
|
let memoryUsed = session.configuration.urlCache?.currentMemoryUsage ?? 0
|