catalyst-core 0.2.0-beta.1 → 0.2.0-beta.3
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 +26 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CustomWebview.kt +381 -78
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +29 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MetricsMonitor.kt +143 -17
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/NativeBridge.kt +7 -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 +162 -26
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/BridgeUtils.kt +56 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +48 -8
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfDeliveryState.kt +36 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/PerfEventBuffer.kt +289 -0
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/ProfilerNavigationState.kt +13 -0
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/CustomWebviewTest.kt +7 -2
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ProfilerLifecycleTest.kt +51 -0
- package/dist/native/bridge/WebBridge.js +2 -2
- package/dist/native/bridge/constants/NativeInterfaces.js +2 -1
- package/dist/native/bridge/perf/collectors/ApiCollector.js +22 -0
- package/dist/native/bridge/perf/collectors/CacheCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/HookCollector.js +43 -0
- package/dist/native/bridge/perf/collectors/InsightsCollector.js +47 -0
- package/dist/native/bridge/perf/collectors/InteractionCollector.js +38 -0
- package/dist/native/bridge/perf/collectors/KeyboardCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/NavigationCollector.js +53 -0
- package/dist/native/bridge/perf/collectors/NetworkTimingCollector.js +7 -0
- package/dist/native/bridge/perf/collectors/RenderCollector.js +40 -0
- package/dist/native/bridge/perf/collectors/ScrollCollector.js +25 -0
- package/dist/native/bridge/perf/collectors/ThreadCollector.js +27 -0
- package/dist/native/bridge/perf/core/constants.js +43 -0
- package/dist/native/bridge/perf/core/measure.js +40 -0
- package/dist/native/bridge/perf/core/session.js +34 -0
- package/dist/native/bridge/perf/core/store.js +1 -0
- package/dist/native/bridge/perf/core/summary.js +1 -0
- package/dist/native/bridge/perf/core/traceExport.js +1 -0
- package/dist/native/bridge/perf/core/waterfalls.js +1 -0
- package/dist/native/bridge/perf/index.js +81 -0
- package/dist/native/buildAndroid/assets.js +2 -2
- package/dist/native/buildIos/config.js +1 -1
- package/dist/native/iosnativeWebView/Sources/Core/Constants/CatalystConstants.swift +19 -0
- package/dist/native/iosnativeWebView/Sources/Core/Constants/ConfigConstants.swift +4 -0
- package/dist/native/iosnativeWebView/Sources/Core/Utils/BootTimingUtility.swift +5 -1
- package/dist/native/iosnativeWebView/Sources/Core/Utils/CacheManager.swift +49 -4
- package/dist/native/iosnativeWebView/Sources/Core/Utils/DisplayLinkPerfMonitor.swift +89 -0
- 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/PerfEventBuffer.swift +373 -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/BridgeFileHandler.swift +35 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/BridgeMessageValidator.swift +24 -11
- package/dist/native/iosnativeWebView/Sources/Core/WebView/NativeBridge.swift +9 -0
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebView.swift +141 -1
- package/dist/native/iosnativeWebView/Sources/Core/WebView/WebViewNavigationDelegate.swift +157 -3
- package/dist/native/iosnativeWebView/iosnativeWebViewTests/BridgeMessageValidatorTests.swift +13 -0
- 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
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
package io.yourname.androidproject
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.net.Uri
|
|
5
|
+
import android.util.Log
|
|
6
|
+
import android.webkit.CookieManager
|
|
7
|
+
import android.webkit.WebResourceResponse
|
|
8
|
+
import kotlinx.coroutines.CoroutineScope
|
|
9
|
+
import kotlinx.coroutines.Dispatchers
|
|
10
|
+
import kotlinx.coroutines.SupervisorJob
|
|
11
|
+
import kotlinx.coroutines.launch
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
import java.io.BufferedInputStream
|
|
14
|
+
import java.io.File
|
|
15
|
+
import java.io.FileInputStream
|
|
16
|
+
import java.io.FileOutputStream
|
|
17
|
+
import java.net.HttpURLConnection
|
|
18
|
+
import java.net.URL
|
|
19
|
+
import java.security.MessageDigest
|
|
20
|
+
|
|
21
|
+
class OfflineCacheService(
|
|
22
|
+
context: Context
|
|
23
|
+
) {
|
|
24
|
+
companion object {
|
|
25
|
+
private const val SNAPSHOT_MAX_AGE_MS = 24 * 60 * 60 * 1000L
|
|
26
|
+
private val ASSET_EXTENSIONS = setOf(
|
|
27
|
+
".js",
|
|
28
|
+
".mjs",
|
|
29
|
+
".css",
|
|
30
|
+
".png",
|
|
31
|
+
".jpg",
|
|
32
|
+
".jpeg",
|
|
33
|
+
".gif",
|
|
34
|
+
".svg",
|
|
35
|
+
".webp",
|
|
36
|
+
".ico",
|
|
37
|
+
".woff",
|
|
38
|
+
".woff2",
|
|
39
|
+
".ttf",
|
|
40
|
+
".eot"
|
|
41
|
+
)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private val TAG = "OfflineCacheService"
|
|
45
|
+
private val FLOW_TAG = "CatalystOfflineFlow"
|
|
46
|
+
private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
|
47
|
+
private val rootDir = File(context.cacheDir, "catalyst_offline")
|
|
48
|
+
private val routeRootDir = File(rootDir, "routes")
|
|
49
|
+
private val manifestFile = File(rootDir, "manifest.json")
|
|
50
|
+
private val ongoingSnapshots = mutableSetOf<String>()
|
|
51
|
+
|
|
52
|
+
@Volatile
|
|
53
|
+
private var manifest: OfflineManifest? = loadCachedManifest()
|
|
54
|
+
|
|
55
|
+
data class OfflineManifest(
|
|
56
|
+
val buildId: String,
|
|
57
|
+
val routes: List<OfflineRoute>
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
data class OfflineRoute(
|
|
61
|
+
val pattern: String,
|
|
62
|
+
val regex: String
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
init {
|
|
66
|
+
rootDir.mkdirs()
|
|
67
|
+
routeRootDir.mkdirs()
|
|
68
|
+
if (BuildConfig.DEBUG) {
|
|
69
|
+
Log.d(FLOW_TAG, "OFFLINE_SERVICE init cacheDir=${rootDir.absolutePath} manifestBuildId=${manifest?.buildId ?: "none"} routes=${manifest?.routes?.size ?: 0}")
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
fun refreshManifestAsync(url: String, headers: Map<String, String> = emptyMap()) {
|
|
74
|
+
if (!isHttpUrl(url)) return
|
|
75
|
+
|
|
76
|
+
if (BuildConfig.DEBUG) {
|
|
77
|
+
Log.d(FLOW_TAG, "MANIFEST refresh-async requested url=$url")
|
|
78
|
+
}
|
|
79
|
+
scope.launch {
|
|
80
|
+
refreshManifestBlocking(url, headers)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fun refreshManifestIfMissing(url: String, headers: Map<String, String> = emptyMap()) {
|
|
85
|
+
if (!isHttpUrl(url) || manifest != null) return
|
|
86
|
+
if (BuildConfig.DEBUG) {
|
|
87
|
+
Log.d(FLOW_TAG, "MANIFEST refresh-blocking reason=missing url=$url")
|
|
88
|
+
}
|
|
89
|
+
refreshManifestBlocking(url, headers)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fun storeRouteSnapshotAsync(url: String, headers: Map<String, String> = emptyMap()) {
|
|
93
|
+
if (!isHttpUrl(url)) return
|
|
94
|
+
val key = normalizeSnapshotUrl(url)
|
|
95
|
+
|
|
96
|
+
val shouldFetch = synchronized(ongoingSnapshots) {
|
|
97
|
+
ongoingSnapshots.add(key)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!shouldFetch) {
|
|
101
|
+
if (BuildConfig.DEBUG) {
|
|
102
|
+
Log.d(FLOW_TAG, "SNAPSHOT skip reason=already-in-flight url=$key")
|
|
103
|
+
}
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (BuildConfig.DEBUG) {
|
|
108
|
+
Log.d(FLOW_TAG, "SNAPSHOT store-requested url=$key")
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
scope.launch {
|
|
112
|
+
try {
|
|
113
|
+
refreshManifestBlocking(url, headers)
|
|
114
|
+
if (!isOfflineRouteUrl(url)) {
|
|
115
|
+
if (BuildConfig.DEBUG) {
|
|
116
|
+
Log.d(FLOW_TAG, "SNAPSHOT skip reason=route-not-eligible url=$url manifestBuildId=${manifest?.buildId ?: "none"} routes=${manifest?.routes?.size ?: 0}")
|
|
117
|
+
}
|
|
118
|
+
return@launch
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
val file = snapshotFileForUrl(key)
|
|
122
|
+
if (isFreshSnapshot(file)) {
|
|
123
|
+
if (BuildConfig.DEBUG) {
|
|
124
|
+
Log.d(FLOW_TAG, "SNAPSHOT skip reason=fresh-existing ageMs=${System.currentTimeMillis() - file.lastModified()} bytes=${file.length()} url=$key path=${file.absolutePath}")
|
|
125
|
+
}
|
|
126
|
+
return@launch
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
val connection = URL(key).openConnection() as HttpURLConnection
|
|
130
|
+
connection.requestMethod = "GET"
|
|
131
|
+
connection.connectTimeout = 15000
|
|
132
|
+
connection.readTimeout = 15000
|
|
133
|
+
connection.setRequestProperty("X-Catalyst-Offline-Snapshot-Fetch", "1")
|
|
134
|
+
headers.forEach { (header, value) ->
|
|
135
|
+
if (shouldForwardHeader(header)) connection.setRequestProperty(header, value)
|
|
136
|
+
}
|
|
137
|
+
CookieManager.getInstance().getCookie(key)?.takeIf { it.isNotBlank() }?.let {
|
|
138
|
+
connection.setRequestProperty("Cookie", it)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
connection.connect()
|
|
142
|
+
val contentType = connection.contentType ?: ""
|
|
143
|
+
if (BuildConfig.DEBUG) {
|
|
144
|
+
Log.d(FLOW_TAG, "SNAPSHOT fetch-response status=${connection.responseCode} contentType=$contentType url=$key")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (connection.responseCode == HttpURLConnection.HTTP_OK &&
|
|
148
|
+
contentType.contains("text/html", ignoreCase = true)
|
|
149
|
+
) {
|
|
150
|
+
val bytes = connection.inputStream.use { it.readBytes() }
|
|
151
|
+
if (bytes.isNotEmpty()) {
|
|
152
|
+
file.parentFile?.mkdirs()
|
|
153
|
+
FileOutputStream(file).use { it.write(bytes) }
|
|
154
|
+
if (BuildConfig.DEBUG) {
|
|
155
|
+
Log.d(TAG, "Stored route snapshot: $key")
|
|
156
|
+
Log.d(FLOW_TAG, "SNAPSHOT stored bytes=${bytes.size} url=$key path=${file.absolutePath}")
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
} else if (BuildConfig.DEBUG) {
|
|
160
|
+
Log.w(FLOW_TAG, "SNAPSHOT not-stored reason=non-html-or-non-200 status=${connection.responseCode} contentType=$contentType url=$key")
|
|
161
|
+
}
|
|
162
|
+
connection.disconnect()
|
|
163
|
+
} catch (e: Exception) {
|
|
164
|
+
if (BuildConfig.DEBUG) {
|
|
165
|
+
Log.w(TAG, "Unable to store route snapshot for $url: ${e.message}")
|
|
166
|
+
Log.w(FLOW_TAG, "SNAPSHOT error phase=store url=$url error=${e.message}", e)
|
|
167
|
+
}
|
|
168
|
+
} finally {
|
|
169
|
+
synchronized(ongoingSnapshots) {
|
|
170
|
+
ongoingSnapshots.remove(key)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fun getRouteSnapshotResponse(url: String): WebResourceResponse? {
|
|
177
|
+
if (!isOfflineRouteUrl(url)) {
|
|
178
|
+
if (BuildConfig.DEBUG) {
|
|
179
|
+
Log.d(FLOW_TAG, "SNAPSHOT miss reason=route-not-eligible url=$url")
|
|
180
|
+
}
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
val file = snapshotFileForUrl(url)
|
|
184
|
+
if (!file.exists()) {
|
|
185
|
+
if (BuildConfig.DEBUG) {
|
|
186
|
+
Log.d(FLOW_TAG, "SNAPSHOT miss reason=file-missing url=$url path=${file.absolutePath}")
|
|
187
|
+
}
|
|
188
|
+
return null
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (BuildConfig.DEBUG) {
|
|
192
|
+
Log.d(FLOW_TAG, "SNAPSHOT hit bytes=${file.length()} url=$url path=${file.absolutePath}")
|
|
193
|
+
}
|
|
194
|
+
return WebResourceResponse(
|
|
195
|
+
"text/html",
|
|
196
|
+
"utf-8",
|
|
197
|
+
200,
|
|
198
|
+
"OK",
|
|
199
|
+
mapOf("X-Catalyst-Offline-Snapshot" to "hit"),
|
|
200
|
+
BufferedInputStream(FileInputStream(file))
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
fun hasRouteSnapshot(url: String): Boolean {
|
|
205
|
+
val eligible = isOfflineRouteUrl(url)
|
|
206
|
+
val file = snapshotFileForUrl(url)
|
|
207
|
+
val exists = eligible && file.exists()
|
|
208
|
+
if (BuildConfig.DEBUG) {
|
|
209
|
+
Log.d(FLOW_TAG, "SNAPSHOT exists-check eligible=$eligible exists=$exists bytes=${if (file.exists()) file.length() else 0} url=$url path=${file.absolutePath}")
|
|
210
|
+
}
|
|
211
|
+
return exists
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
fun isOfflineRouteUrl(url: String): Boolean {
|
|
215
|
+
if (!isHttpUrl(url)) return false
|
|
216
|
+
val currentManifest = manifest ?: return false
|
|
217
|
+
val path = Uri.parse(url).path ?: "/"
|
|
218
|
+
|
|
219
|
+
val matchedRoute = currentManifest.routes.firstOrNull { route ->
|
|
220
|
+
try {
|
|
221
|
+
Regex(route.regex, RegexOption.IGNORE_CASE).matches(path)
|
|
222
|
+
} catch (_: Exception) {
|
|
223
|
+
false
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (BuildConfig.DEBUG) {
|
|
227
|
+
Log.d(FLOW_TAG, "ROUTE match=${matchedRoute != null} path=$path pattern=${matchedRoute?.pattern ?: "none"} regex=${matchedRoute?.regex ?: "none"} buildId=${currentManifest.buildId} url=$url")
|
|
228
|
+
}
|
|
229
|
+
return matchedRoute != null
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
fun shouldCacheAssetUrl(url: String): Boolean {
|
|
233
|
+
if (!isHttpUrl(url)) return false
|
|
234
|
+
val path = Uri.parse(url).path?.lowercase() ?: return false
|
|
235
|
+
return ASSET_EXTENSIONS.any { path.endsWith(it) }
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
fun clearAll() {
|
|
239
|
+
try {
|
|
240
|
+
deleteRecursively(rootDir)
|
|
241
|
+
rootDir.mkdirs()
|
|
242
|
+
routeRootDir.mkdirs()
|
|
243
|
+
manifest = null
|
|
244
|
+
if (BuildConfig.DEBUG) {
|
|
245
|
+
Log.d(TAG, "Offline route snapshots cleared")
|
|
246
|
+
}
|
|
247
|
+
} catch (e: Exception) {
|
|
248
|
+
Log.e(TAG, "Unable to clear offline cache: ${e.message}")
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private fun refreshManifestBlocking(url: String, headers: Map<String, String>): OfflineManifest? {
|
|
253
|
+
return try {
|
|
254
|
+
val manifestUrl = manifestUrlFor(url)
|
|
255
|
+
val connection = URL(manifestUrl).openConnection() as HttpURLConnection
|
|
256
|
+
connection.requestMethod = "GET"
|
|
257
|
+
connection.connectTimeout = 10000
|
|
258
|
+
connection.readTimeout = 10000
|
|
259
|
+
headers.forEach { (header, value) ->
|
|
260
|
+
if (shouldForwardHeader(header)) connection.setRequestProperty(header, value)
|
|
261
|
+
}
|
|
262
|
+
CookieManager.getInstance().getCookie(manifestUrl)?.takeIf { it.isNotBlank() }?.let {
|
|
263
|
+
connection.setRequestProperty("Cookie", it)
|
|
264
|
+
}
|
|
265
|
+
connection.connect()
|
|
266
|
+
|
|
267
|
+
if (connection.responseCode != HttpURLConnection.HTTP_OK) {
|
|
268
|
+
if (BuildConfig.DEBUG) {
|
|
269
|
+
Log.w(FLOW_TAG, "MANIFEST refresh-response status=${connection.responseCode} url=$manifestUrl keepingBuildId=${manifest?.buildId ?: "none"}")
|
|
270
|
+
}
|
|
271
|
+
connection.disconnect()
|
|
272
|
+
return manifest
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
val body = connection.inputStream.use { it.readBytes().toString(Charsets.UTF_8) }
|
|
276
|
+
connection.disconnect()
|
|
277
|
+
val parsed = parseManifest(body)
|
|
278
|
+
manifestFile.writeText(body)
|
|
279
|
+
manifest = parsed
|
|
280
|
+
if (BuildConfig.DEBUG) {
|
|
281
|
+
Log.d(FLOW_TAG, "MANIFEST refreshed buildId=${parsed.buildId} routes=${parsed.routes.size} url=$manifestUrl")
|
|
282
|
+
parsed.routes.forEach { route ->
|
|
283
|
+
Log.d(FLOW_TAG, "MANIFEST route pattern=${route.pattern} regex=${route.regex}")
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
parsed
|
|
287
|
+
} catch (e: Exception) {
|
|
288
|
+
if (BuildConfig.DEBUG) {
|
|
289
|
+
Log.w(TAG, "Unable to refresh offline manifest: ${e.message}")
|
|
290
|
+
Log.w(FLOW_TAG, "MANIFEST refresh-error url=$url error=${e.message}", e)
|
|
291
|
+
}
|
|
292
|
+
manifest
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private fun loadCachedManifest(): OfflineManifest? {
|
|
297
|
+
return try {
|
|
298
|
+
if (!manifestFile.exists()) return null
|
|
299
|
+
parseManifest(manifestFile.readText())
|
|
300
|
+
} catch (_: Exception) {
|
|
301
|
+
null
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private fun parseManifest(raw: String): OfflineManifest {
|
|
306
|
+
val json = JSONObject(raw)
|
|
307
|
+
val buildId = json.optString("buildId", "unknown")
|
|
308
|
+
val routesJson = json.optJSONArray("routes")
|
|
309
|
+
val routes = mutableListOf<OfflineRoute>()
|
|
310
|
+
if (routesJson != null) {
|
|
311
|
+
for (index in 0 until routesJson.length()) {
|
|
312
|
+
val route = routesJson.optJSONObject(index) ?: continue
|
|
313
|
+
val pattern = route.optString("pattern")
|
|
314
|
+
val regex = route.optString("regex")
|
|
315
|
+
if (pattern.isNotBlank() && regex.isNotBlank()) {
|
|
316
|
+
routes.add(OfflineRoute(pattern, regex))
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return OfflineManifest(buildId, routes)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
private fun snapshotFileForUrl(url: String): File {
|
|
325
|
+
val normalizedUrl = normalizeSnapshotUrl(url)
|
|
326
|
+
val namespace = manifest?.let { "${originFor(normalizedUrl)}:${it.buildId}" } ?: originFor(normalizedUrl)
|
|
327
|
+
val namespaceDir = File(routeRootDir, hash(namespace))
|
|
328
|
+
return File(namespaceDir, "${hash(normalizedUrl)}.html")
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private fun manifestUrlFor(url: String): String {
|
|
332
|
+
val uri = Uri.parse(url)
|
|
333
|
+
return "${uri.scheme}://${uri.authority}/catalyst-offline-manifest.json"
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private fun originFor(url: String): String {
|
|
337
|
+
val uri = Uri.parse(url)
|
|
338
|
+
return "${uri.scheme}://${uri.authority}"
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
private fun normalizeSnapshotUrl(url: String): String {
|
|
342
|
+
val uri = Uri.parse(url)
|
|
343
|
+
return uri.buildUpon().fragment(null).build().toString()
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
private fun isHttpUrl(url: String): Boolean {
|
|
347
|
+
val scheme = Uri.parse(url).scheme?.lowercase()
|
|
348
|
+
return scheme == "http" || scheme == "https"
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private fun shouldForwardHeader(header: String): Boolean {
|
|
352
|
+
return !header.equals("Cookie", ignoreCase = true) &&
|
|
353
|
+
!header.equals("Host", ignoreCase = true) &&
|
|
354
|
+
!header.equals("Cache-Control", ignoreCase = true) &&
|
|
355
|
+
!header.equals("Pragma", ignoreCase = true)
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
private fun isFreshSnapshot(file: File): Boolean {
|
|
359
|
+
return file.exists() && System.currentTimeMillis() - file.lastModified() < SNAPSHOT_MAX_AGE_MS
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private fun hash(value: String): String {
|
|
363
|
+
val digest = MessageDigest.getInstance("SHA-256").digest(value.toByteArray())
|
|
364
|
+
return digest.joinToString("") { "%02x".format(it) }
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private fun deleteRecursively(file: File) {
|
|
368
|
+
if (!file.exists()) return
|
|
369
|
+
if (file.isDirectory) {
|
|
370
|
+
file.listFiles()?.forEach { deleteRecursively(it) }
|
|
371
|
+
}
|
|
372
|
+
file.delete()
|
|
373
|
+
}
|
|
374
|
+
}
|