@hot-updater/react-native 1.0.0-rc.16 → 1.0.0-rc.17

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.
Files changed (316) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +491 -388
  2. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +5 -11
  3. package/android/src/main/java/com/hotupdater/FileManagerService.kt +45 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +290 -12
  5. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +35 -19
  6. package/android/src/main/java/com/hotupdater/InstallIdentityService.kt +37 -0
  7. package/android/src/main/java/com/hotupdater/KeyValueStorageService.kt +86 -0
  8. package/android/src/main/java/com/hotupdater/NetworkState.kt +31 -0
  9. package/android/src/main/java/com/hotupdater/NoBackupStorage.kt +17 -0
  10. package/android/src/main/java/com/hotupdater/OkHttpDownloadService.kt +110 -52
  11. package/android/src/main/java/com/hotupdater/PreviousProcessExit.kt +89 -0
  12. package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +34 -0
  13. package/android/src/newarch/HotUpdaterModule.kt +74 -69
  14. package/android/src/oldarch/HotUpdaterModule.kt +63 -59
  15. package/android/src/oldarch/HotUpdaterSpec.kt +4 -6
  16. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +1 -1
  17. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +446 -61
  18. package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +220 -0
  19. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +19 -27
  20. package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +71 -0
  21. package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +106 -0
  22. package/android/src/test/java/com/hotupdater/NoBackupContext.kt +11 -0
  23. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +309 -23
  24. package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +57 -0
  25. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +238 -157
  26. package/ios/HotUpdater/Internal/BundleMetadata.swift +3 -53
  27. package/ios/HotUpdater/Internal/FileManagerService.swift +48 -0
  28. package/ios/HotUpdater/Internal/HotUpdater.mm +8 -18
  29. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +48 -29
  30. package/ios/HotUpdater/Internal/KeyValueStorageService.swift +71 -0
  31. package/ios/HotUpdater/Internal/URLSessionDownloadService.swift +108 -1
  32. package/ios/HotUpdater/Internal/UpdateFailure.swift +390 -0
  33. package/lib/commonjs/appReady.js +111 -0
  34. package/lib/commonjs/appReady.js.map +1 -0
  35. package/lib/commonjs/appReady.spec.js +360 -0
  36. package/lib/commonjs/appReady.spec.js.map +1 -0
  37. package/lib/commonjs/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  38. package/lib/commonjs/appReady.test-utils.js.map +1 -0
  39. package/lib/commonjs/baseURL.js +16 -0
  40. package/lib/commonjs/baseURL.js.map +1 -0
  41. package/lib/commonjs/checkForUpdate.js +104 -56
  42. package/lib/commonjs/checkForUpdate.js.map +1 -1
  43. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +337 -74
  44. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  45. package/lib/commonjs/clientPlugin.js +49 -0
  46. package/lib/commonjs/clientPlugin.js.map +1 -0
  47. package/lib/commonjs/entryImportGraph.spec.js +50 -0
  48. package/lib/commonjs/entryImportGraph.spec.js.map +1 -0
  49. package/lib/commonjs/fetchJSON.js +2 -6
  50. package/lib/commonjs/fetchJSON.js.map +1 -1
  51. package/lib/commonjs/httpClient.js +18 -165
  52. package/lib/commonjs/httpClient.js.map +1 -1
  53. package/lib/commonjs/httpClient.spec.js +26 -199
  54. package/lib/commonjs/httpClient.spec.js.map +1 -1
  55. package/lib/commonjs/index.js +49 -36
  56. package/lib/commonjs/index.js.map +1 -1
  57. package/lib/commonjs/index.spec.js +108 -123
  58. package/lib/commonjs/index.spec.js.map +1 -1
  59. package/lib/commonjs/native.js +67 -48
  60. package/lib/commonjs/native.js.map +1 -1
  61. package/lib/commonjs/native.spec.js +80 -58
  62. package/lib/commonjs/native.spec.js.map +1 -1
  63. package/lib/commonjs/pluginHost.js +208 -0
  64. package/lib/commonjs/pluginHost.js.map +1 -0
  65. package/lib/commonjs/pluginHost.spec.js +292 -0
  66. package/lib/commonjs/pluginHost.spec.js.map +1 -0
  67. package/lib/commonjs/plugins/insights/eventId.js +129 -0
  68. package/lib/commonjs/plugins/insights/eventId.js.map +1 -0
  69. package/lib/commonjs/plugins/insights/eventId.spec.js +47 -0
  70. package/lib/commonjs/plugins/insights/eventId.spec.js.map +1 -0
  71. package/lib/commonjs/plugins/insights/index.js +272 -0
  72. package/lib/commonjs/plugins/insights/index.js.map +1 -0
  73. package/lib/commonjs/plugins/insights/insights.spec.js +640 -0
  74. package/lib/commonjs/plugins/insights/insights.spec.js.map +1 -0
  75. package/lib/commonjs/plugins/insights/sender.js +128 -0
  76. package/lib/commonjs/plugins/insights/sender.js.map +1 -0
  77. package/lib/commonjs/plugins/insights/sender.spec.js +265 -0
  78. package/lib/commonjs/plugins/insights/sender.spec.js.map +1 -0
  79. package/lib/commonjs/plugins/insights/state.js +145 -0
  80. package/lib/commonjs/plugins/insights/state.js.map +1 -0
  81. package/lib/commonjs/releaseCatalogCache.js +13 -2
  82. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  83. package/lib/commonjs/releaseCatalogCache.spec.js +18 -0
  84. package/lib/commonjs/releaseCatalogCache.spec.js.map +1 -1
  85. package/lib/commonjs/sdkVersion.js +1 -1
  86. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  87. package/lib/commonjs/updateError.js +132 -0
  88. package/lib/commonjs/updateError.js.map +1 -0
  89. package/lib/commonjs/updateError.spec.js +149 -0
  90. package/lib/commonjs/updateError.spec.js.map +1 -0
  91. package/lib/commonjs/wrap.js +43 -25
  92. package/lib/commonjs/wrap.js.map +1 -1
  93. package/lib/commonjs/wrap.progress.spec.js +195 -0
  94. package/lib/commonjs/wrap.progress.spec.js.map +1 -0
  95. package/lib/commonjs/wrap.spec.js +94 -230
  96. package/lib/commonjs/wrap.spec.js.map +1 -1
  97. package/lib/module/appReady.js +103 -0
  98. package/lib/module/appReady.js.map +1 -0
  99. package/lib/module/appReady.spec.js +359 -0
  100. package/lib/module/appReady.spec.js.map +1 -0
  101. package/lib/module/{notifyAppReadyInsights.test-utils.js → appReady.test-utils.js} +4 -3
  102. package/lib/module/appReady.test-utils.js.map +1 -0
  103. package/lib/module/baseURL.js +11 -0
  104. package/lib/module/baseURL.js.map +1 -0
  105. package/lib/module/checkForUpdate.js +103 -57
  106. package/lib/module/checkForUpdate.js.map +1 -1
  107. package/lib/module/checkForUpdate.releaseCatalog.spec.js +337 -74
  108. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  109. package/lib/module/clientPlugin.js +44 -0
  110. package/lib/module/clientPlugin.js.map +1 -0
  111. package/lib/module/entryImportGraph.spec.js +50 -0
  112. package/lib/module/entryImportGraph.spec.js.map +1 -0
  113. package/lib/module/fetchJSON.js +2 -6
  114. package/lib/module/fetchJSON.js.map +1 -1
  115. package/lib/module/httpClient.js +18 -165
  116. package/lib/module/httpClient.js.map +1 -1
  117. package/lib/module/httpClient.spec.js +27 -200
  118. package/lib/module/httpClient.spec.js.map +1 -1
  119. package/lib/module/index.js +44 -38
  120. package/lib/module/index.js.map +1 -1
  121. package/lib/module/index.spec.js +108 -123
  122. package/lib/module/index.spec.js.map +1 -1
  123. package/lib/module/native.js +63 -44
  124. package/lib/module/native.js.map +1 -1
  125. package/lib/module/native.spec.js +80 -58
  126. package/lib/module/native.spec.js.map +1 -1
  127. package/lib/module/pluginHost.js +200 -0
  128. package/lib/module/pluginHost.js.map +1 -0
  129. package/lib/module/pluginHost.spec.js +291 -0
  130. package/lib/module/pluginHost.spec.js.map +1 -0
  131. package/lib/module/plugins/insights/eventId.js +122 -0
  132. package/lib/module/plugins/insights/eventId.js.map +1 -0
  133. package/lib/module/plugins/insights/eventId.spec.js +47 -0
  134. package/lib/module/plugins/insights/eventId.spec.js.map +1 -0
  135. package/lib/module/plugins/insights/index.js +268 -0
  136. package/lib/module/plugins/insights/index.js.map +1 -0
  137. package/lib/module/plugins/insights/insights.spec.js +639 -0
  138. package/lib/module/plugins/insights/insights.spec.js.map +1 -0
  139. package/lib/module/plugins/insights/sender.js +123 -0
  140. package/lib/module/plugins/insights/sender.js.map +1 -0
  141. package/lib/module/plugins/insights/sender.spec.js +265 -0
  142. package/lib/module/plugins/insights/sender.spec.js.map +1 -0
  143. package/lib/module/plugins/insights/state.js +139 -0
  144. package/lib/module/plugins/insights/state.js.map +1 -0
  145. package/lib/module/releaseCatalogCache.js +13 -2
  146. package/lib/module/releaseCatalogCache.js.map +1 -1
  147. package/lib/module/releaseCatalogCache.spec.js +18 -0
  148. package/lib/module/releaseCatalogCache.spec.js.map +1 -1
  149. package/lib/module/sdkVersion.js +1 -1
  150. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  151. package/lib/module/updateError.js +126 -0
  152. package/lib/module/updateError.js.map +1 -0
  153. package/lib/module/updateError.spec.js +149 -0
  154. package/lib/module/updateError.spec.js.map +1 -0
  155. package/lib/module/wrap.js +43 -25
  156. package/lib/module/wrap.js.map +1 -1
  157. package/lib/module/wrap.progress.spec.js +196 -0
  158. package/lib/module/wrap.progress.spec.js.map +1 -0
  159. package/lib/module/wrap.spec.js +93 -229
  160. package/lib/module/wrap.spec.js.map +1 -1
  161. package/lib/typescript/commonjs/appReady.d.ts +23 -0
  162. package/lib/typescript/commonjs/appReady.d.ts.map +1 -0
  163. package/lib/typescript/commonjs/appReady.test-utils.d.ts +9 -0
  164. package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +1 -0
  165. package/lib/typescript/commonjs/baseURL.d.ts +4 -0
  166. package/lib/typescript/commonjs/baseURL.d.ts.map +1 -0
  167. package/lib/typescript/commonjs/checkForUpdate.d.ts +15 -2
  168. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  169. package/lib/typescript/commonjs/clientPlugin.d.ts +185 -0
  170. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -0
  171. package/lib/typescript/commonjs/fetchJSON.d.ts +2 -3
  172. package/lib/typescript/commonjs/fetchJSON.d.ts.map +1 -1
  173. package/lib/typescript/commonjs/httpClient.d.ts +2 -36
  174. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  175. package/lib/typescript/commonjs/index.d.ts +3 -6
  176. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  177. package/lib/typescript/commonjs/native.d.ts +29 -15
  178. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  179. package/lib/typescript/commonjs/pluginHost.d.ts +29 -0
  180. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -0
  181. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +18 -0
  182. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +1 -0
  183. package/lib/typescript/commonjs/plugins/insights/index.d.ts +40 -0
  184. package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +1 -0
  185. package/lib/typescript/commonjs/plugins/insights/sender.d.ts +74 -0
  186. package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +1 -0
  187. package/lib/typescript/commonjs/plugins/insights/state.d.ts +46 -0
  188. package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +1 -0
  189. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +5 -1
  190. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  191. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  192. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +34 -12
  193. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  194. package/lib/typescript/commonjs/updateError.d.ts +26 -0
  195. package/lib/typescript/commonjs/updateError.d.ts.map +1 -0
  196. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  197. package/lib/typescript/commonjs/wrap.types.d.ts +6 -10
  198. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  199. package/lib/typescript/module/appReady.d.ts +23 -0
  200. package/lib/typescript/module/appReady.d.ts.map +1 -0
  201. package/lib/typescript/module/appReady.test-utils.d.ts +9 -0
  202. package/lib/typescript/module/appReady.test-utils.d.ts.map +1 -0
  203. package/lib/typescript/module/baseURL.d.ts +4 -0
  204. package/lib/typescript/module/baseURL.d.ts.map +1 -0
  205. package/lib/typescript/module/checkForUpdate.d.ts +15 -2
  206. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  207. package/lib/typescript/module/clientPlugin.d.ts +185 -0
  208. package/lib/typescript/module/clientPlugin.d.ts.map +1 -0
  209. package/lib/typescript/module/fetchJSON.d.ts +2 -3
  210. package/lib/typescript/module/fetchJSON.d.ts.map +1 -1
  211. package/lib/typescript/module/httpClient.d.ts +2 -36
  212. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  213. package/lib/typescript/module/index.d.ts +3 -6
  214. package/lib/typescript/module/index.d.ts.map +1 -1
  215. package/lib/typescript/module/native.d.ts +29 -15
  216. package/lib/typescript/module/native.d.ts.map +1 -1
  217. package/lib/typescript/module/pluginHost.d.ts +29 -0
  218. package/lib/typescript/module/pluginHost.d.ts.map +1 -0
  219. package/lib/typescript/module/plugins/insights/eventId.d.ts +18 -0
  220. package/lib/typescript/module/plugins/insights/eventId.d.ts.map +1 -0
  221. package/lib/typescript/module/plugins/insights/index.d.ts +40 -0
  222. package/lib/typescript/module/plugins/insights/index.d.ts.map +1 -0
  223. package/lib/typescript/module/plugins/insights/sender.d.ts +74 -0
  224. package/lib/typescript/module/plugins/insights/sender.d.ts.map +1 -0
  225. package/lib/typescript/module/plugins/insights/state.d.ts +46 -0
  226. package/lib/typescript/module/plugins/insights/state.d.ts.map +1 -0
  227. package/lib/typescript/module/releaseCatalogCache.d.ts +5 -1
  228. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  229. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  230. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +34 -12
  231. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  232. package/lib/typescript/module/updateError.d.ts +26 -0
  233. package/lib/typescript/module/updateError.d.ts.map +1 -0
  234. package/lib/typescript/module/wrap.d.ts.map +1 -1
  235. package/lib/typescript/module/wrap.types.d.ts +6 -10
  236. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  237. package/package.json +14 -2
  238. package/plugins/insights/package.json +8 -0
  239. package/src/appReady.spec.ts +393 -0
  240. package/src/{notifyAppReadyInsights.test-utils.ts → appReady.test-utils.ts} +7 -7
  241. package/src/appReady.ts +149 -0
  242. package/src/baseURL.ts +15 -0
  243. package/src/checkForUpdate.releaseCatalog.spec.ts +370 -76
  244. package/src/checkForUpdate.ts +135 -80
  245. package/src/clientPlugin.ts +231 -0
  246. package/src/entryImportGraph.spec.ts +65 -0
  247. package/src/fetchJSON.ts +3 -10
  248. package/src/httpClient.spec.ts +30 -227
  249. package/src/httpClient.ts +20 -256
  250. package/src/index.spec.ts +117 -93
  251. package/src/index.ts +67 -52
  252. package/src/native.spec.ts +98 -72
  253. package/src/native.ts +93 -77
  254. package/src/pluginHost.spec.ts +297 -0
  255. package/src/pluginHost.ts +262 -0
  256. package/src/plugins/insights/eventId.spec.ts +60 -0
  257. package/src/plugins/insights/eventId.ts +171 -0
  258. package/src/plugins/insights/index.ts +367 -0
  259. package/src/plugins/insights/insights.spec.ts +732 -0
  260. package/src/plugins/insights/sender.spec.ts +287 -0
  261. package/src/plugins/insights/sender.ts +221 -0
  262. package/src/plugins/insights/state.ts +184 -0
  263. package/src/releaseCatalogCache.spec.ts +22 -0
  264. package/src/releaseCatalogCache.ts +19 -4
  265. package/src/sdkVersion.ts +1 -1
  266. package/src/specs/NativeHotUpdater.ts +34 -13
  267. package/src/updateError.spec.ts +144 -0
  268. package/src/updateError.ts +153 -0
  269. package/src/wrap.progress.spec.ts +232 -0
  270. package/src/wrap.spec.ts +100 -283
  271. package/src/wrap.tsx +57 -26
  272. package/src/wrap.types.ts +6 -12
  273. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js +0 -225
  274. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  275. package/lib/commonjs/notifyAppReadyInsights.js +0 -188
  276. package/lib/commonjs/notifyAppReadyInsights.js.map +0 -1
  277. package/lib/commonjs/notifyAppReadyInsights.spec.js +0 -378
  278. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +0 -1
  279. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +0 -1
  280. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js +0 -171
  281. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +0 -1
  282. package/lib/commonjs/uuidv7.js +0 -41
  283. package/lib/commonjs/uuidv7.js.map +0 -1
  284. package/lib/commonjs/uuidv7.spec.js +0 -30
  285. package/lib/commonjs/uuidv7.spec.js.map +0 -1
  286. package/lib/module/notifyAppReadyInsights.boundary.spec.js +0 -224
  287. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +0 -1
  288. package/lib/module/notifyAppReadyInsights.js +0 -181
  289. package/lib/module/notifyAppReadyInsights.js.map +0 -1
  290. package/lib/module/notifyAppReadyInsights.spec.js +0 -377
  291. package/lib/module/notifyAppReadyInsights.spec.js.map +0 -1
  292. package/lib/module/notifyAppReadyInsights.test-utils.js.map +0 -1
  293. package/lib/module/notifyAppReadyInsights.transport.spec.js +0 -170
  294. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +0 -1
  295. package/lib/module/uuidv7.js +0 -36
  296. package/lib/module/uuidv7.js.map +0 -1
  297. package/lib/module/uuidv7.spec.js +0 -30
  298. package/lib/module/uuidv7.spec.js.map +0 -1
  299. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +0 -25
  300. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +0 -1
  301. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +0 -8
  302. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  303. package/lib/typescript/commonjs/uuidv7.d.ts +0 -10
  304. package/lib/typescript/commonjs/uuidv7.d.ts.map +0 -1
  305. package/lib/typescript/module/notifyAppReadyInsights.d.ts +0 -25
  306. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +0 -1
  307. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +0 -8
  308. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +0 -1
  309. package/lib/typescript/module/uuidv7.d.ts +0 -10
  310. package/lib/typescript/module/uuidv7.d.ts.map +0 -1
  311. package/src/notifyAppReadyInsights.boundary.spec.ts +0 -242
  312. package/src/notifyAppReadyInsights.spec.ts +0 -362
  313. package/src/notifyAppReadyInsights.transport.spec.ts +0 -196
  314. package/src/notifyAppReadyInsights.ts +0 -293
  315. package/src/uuidv7.spec.ts +0 -25
  316. package/src/uuidv7.ts +0 -38
@@ -383,6 +383,8 @@ public struct LaunchReport: Codable {
383
383
  }
384
384
  }
385
385
 
386
+ /// The random id created once per app installation. It lives in
387
+ /// `NoBackupStorage`, so a device backup never carries it to another device.
386
388
  public struct InstallIdentity: Codable {
387
389
  static let installIdentityFilename = "install-identity.json"
388
390
 
@@ -412,11 +414,7 @@ public struct InstallIdentity: Codable {
412
414
  let encoder = JSONEncoder()
413
415
  encoder.outputFormatting = .prettyPrinted
414
416
  let data = try encoder.encode(self)
415
- let directory = file.deletingLastPathComponent()
416
- if !FileManager.default.fileExists(atPath: directory.path) {
417
- try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
418
- }
419
- try data.write(to: file)
417
+ try NoBackupStorage.write(data, to: file)
420
418
  return true
421
419
  } catch {
422
420
  print("[InstallIdentity] Failed to save install identity: \(error)")
@@ -424,51 +422,3 @@ public struct InstallIdentity: Codable {
424
422
  }
425
423
  }
426
424
  }
427
-
428
- public struct UserIdentity: Codable {
429
- static let userIdentityFilename = "user-identity.json"
430
-
431
- let userId: String?
432
- let username: String?
433
-
434
- init(userId: String?, username: String?) {
435
- self.userId = userId
436
- self.username = username
437
- }
438
-
439
- var isEmpty: Bool {
440
- userId == nil && username == nil
441
- }
442
-
443
- static func load(from file: URL) -> UserIdentity? {
444
- guard FileManager.default.fileExists(atPath: file.path) else {
445
- return nil
446
- }
447
-
448
- do {
449
- let data = try Data(contentsOf: file)
450
- let identity = try JSONDecoder().decode(UserIdentity.self, from: data)
451
- return identity.isEmpty ? nil : identity
452
- } catch {
453
- print("[UserIdentity] Failed to load user identity: \(error)")
454
- return nil
455
- }
456
- }
457
-
458
- func save(to file: URL) -> Bool {
459
- do {
460
- let encoder = JSONEncoder()
461
- encoder.outputFormatting = .prettyPrinted
462
- let data = try encoder.encode(self)
463
- let directory = file.deletingLastPathComponent()
464
- if !FileManager.default.fileExists(atPath: directory.path) {
465
- try FileManager.default.createDirectory(at: directory, withIntermediateDirectories: true)
466
- }
467
- try data.write(to: file)
468
- return true
469
- } catch {
470
- print("[UserIdentity] Failed to save user identity: \(error)")
471
- return false
472
- }
473
- }
474
- }
@@ -17,6 +17,45 @@ protocol FileSystemService {
17
17
  func contentsOfDirectory(atPath path: String) throws -> [String]
18
18
  func attributesOfItem(atPath path: String) throws -> [FileAttributeKey: Any]
19
19
  func documentsPath() -> String
20
+ /// Base directory for data that lives as long as the installation.
21
+ func applicationSupportPath() -> String
22
+ }
23
+
24
+ /// `Application Support/HotUpdater`: data the SDK keeps for the life of an
25
+ /// installation, such as the install id and plugin storage. Device backups
26
+ /// skip it, so restoring a backup onto another device never copies it there.
27
+ enum NoBackupStorage {
28
+ static let directoryName = "HotUpdater"
29
+
30
+ static func directory(in fileSystem: FileSystemService) -> URL {
31
+ URL(fileURLWithPath: fileSystem.applicationSupportPath(), isDirectory: true)
32
+ .appendingPathComponent(directoryName, isDirectory: true)
33
+ }
34
+
35
+ /// Atomically replaces `file` with `data`, creating its directory when
36
+ /// needed, and excludes the directory and the file from device backups.
37
+ static func write(_ data: Data, to file: URL) throws {
38
+ let directory = file.deletingLastPathComponent()
39
+ try FileManager.default.createDirectory(
40
+ at: directory,
41
+ withIntermediateDirectories: true
42
+ )
43
+ excludeFromBackup(directory)
44
+ try data.write(to: file, options: .atomic)
45
+ // An atomic write replaces the file, which drops its previous flag.
46
+ excludeFromBackup(file)
47
+ }
48
+
49
+ static func excludeFromBackup(_ url: URL) {
50
+ var url = url
51
+ var values = URLResourceValues()
52
+ values.isExcludedFromBackup = true
53
+ do {
54
+ try url.setResourceValues(values)
55
+ } catch {
56
+ NSLog("[NoBackupStorage] Failed to exclude \(url.path) from backups: \(error)")
57
+ }
58
+ }
20
59
  }
21
60
 
22
61
  class FileManagerService: FileSystemService {
@@ -89,4 +128,13 @@ class FileManagerService: FileSystemService {
89
128
  return NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0]
90
129
  #endif
91
130
  }
131
+
132
+ func applicationSupportPath() -> String {
133
+ #if os(tvOS)
134
+ // tvOS apps can only write purgeable files, so use Caches like documentsPath().
135
+ return NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0]
136
+ #else
137
+ return NSSearchPathForDirectoriesInDomains(.applicationSupportDirectory, .userDomainMask, true)[0]
138
+ #endif
139
+ }
92
140
  }
@@ -660,19 +660,14 @@ RCT_EXPORT_MODULE();
660
660
  return [impl getInstallId];
661
661
  }
662
662
 
663
- - (NSString * _Nullable)getUserId {
663
+ - (NSString * _Nullable)getStorageItem:(NSString *)key {
664
664
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
665
- return [impl getUserId];
665
+ return [impl getStorageItem:key];
666
666
  }
667
667
 
668
- - (NSString * _Nullable)getUsername {
668
+ - (void)setStorageItem:(NSString *)key value:(NSString * _Nullable)value {
669
669
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
670
- return [impl getUsername];
671
- }
672
-
673
- - (void)setUser:(NSString * _Nullable)userId username:(NSString * _Nullable)username {
674
- HotUpdaterImpl *impl = [HotUpdater sharedImpl];
675
- [impl setUser:userId username:username];
670
+ [impl setStorageItem:key value:value];
676
671
  }
677
672
 
678
673
  - (NSString * _Nullable)getBaseURL {
@@ -830,19 +825,14 @@ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getInstallId) {
830
825
  return [impl getInstallId];
831
826
  }
832
827
 
833
- RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getUserId) {
834
- HotUpdaterImpl *impl = [HotUpdater sharedImpl];
835
- return [impl getUserId];
836
- }
837
-
838
- RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getUsername) {
828
+ RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getStorageItem:(NSString *)key) {
839
829
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
840
- return [impl getUsername];
830
+ return [impl getStorageItem:key];
841
831
  }
842
832
 
843
- RCT_EXPORT_METHOD(setUser:(NSString * _Nullable)userId username:(NSString * _Nullable)username) {
833
+ RCT_EXPORT_METHOD(setStorageItem:(NSString *)key value:(NSString * _Nullable)value) {
844
834
  HotUpdaterImpl *impl = [HotUpdater sharedImpl];
845
- [impl setUser:userId username:username];
835
+ [impl setStorageItem:key value:value];
846
836
  }
847
837
 
848
838
  RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(getBaseURL) {
@@ -56,6 +56,7 @@ private func hotUpdaterGetMinBundleId() -> String {
56
56
  private let cohortService: CohortService
57
57
  private let recoveryManager: HotUpdaterRecoveryManager
58
58
  private let releaseCatalogCache: ReleaseCatalogCacheService
59
+ private let keyValueStorage: KeyValueStorageService
59
60
  private var currentLaunchSelection: LaunchSelection?
60
61
 
61
62
  private static let DEFAULT_CHANNEL = "production"
@@ -99,7 +100,10 @@ private func hotUpdaterGetMinBundleId() -> String {
99
100
  bundleStorage: bundleStorage,
100
101
  preferences: preferences,
101
102
  recoveryManager: recoveryManager,
102
- releaseCatalogCache: ReleaseCatalogCacheService()
103
+ releaseCatalogCache: ReleaseCatalogCacheService(),
104
+ keyValueStorage: KeyValueStorageService(
105
+ directory: NoBackupStorage.directory(in: fileSystem)
106
+ )
103
107
  )
104
108
  }
105
109
 
@@ -107,18 +111,23 @@ private func hotUpdaterGetMinBundleId() -> String {
107
111
  * Primary initializer with dependency injection.
108
112
  * @param bundleStorage Service for bundle storage operations
109
113
  * @param preferences Service for preference storage
114
+ * @param keyValueStorage Persistent key-value store for client plugins
110
115
  */
111
116
  internal init(
112
117
  bundleStorage: BundleStorageService,
113
118
  preferences: PreferencesService,
114
119
  recoveryManager: HotUpdaterRecoveryManager,
115
- releaseCatalogCache: ReleaseCatalogCacheService = ReleaseCatalogCacheService()
120
+ releaseCatalogCache: ReleaseCatalogCacheService = ReleaseCatalogCacheService(),
121
+ keyValueStorage: KeyValueStorageService = KeyValueStorageService(
122
+ directory: NoBackupStorage.directory(in: FileManagerService())
123
+ )
116
124
  ) {
117
125
  self.bundleStorage = bundleStorage
118
126
  self.preferences = preferences
119
127
  self.cohortService = CohortService()
120
128
  self.recoveryManager = recoveryManager
121
129
  self.releaseCatalogCache = releaseCatalogCache
130
+ self.keyValueStorage = keyValueStorage
122
131
  super.init()
123
132
 
124
133
  // Configure preferences with isolation key
@@ -244,8 +253,10 @@ private func hotUpdaterGetMinBundleId() -> String {
244
253
  * Updates the bundle from JavaScript bridge.
245
254
  * This method acts as the primary error boundary for all bundle operations.
246
255
  * @param params Dictionary with bundleId, manifest, and asset parameters
247
- * @param resolve Promise resolve callback
248
- * @param reject Promise reject callback
256
+ * @param resolve Promise resolve callback, called with the staged bundle's
257
+ * `{ delivery, patchFallback }`
258
+ * @param reject Promise reject callback; download and install failures
259
+ * carry `stage`, `reason`, and their details in the error's userInfo
249
260
  */
250
261
  public func updateBundle(_ params: NSDictionary?,
251
262
  resolver resolve: @escaping RCTPromiseResolveBlock,
@@ -260,33 +271,35 @@ private func hotUpdaterGetMinBundleId() -> String {
260
271
  return
261
272
  }
262
273
 
274
+ // Invalid update parameters are an invalid update response.
263
275
  guard let bundleId = data["bundleId"] as? String, !bundleId.isEmpty else {
264
- let error = NSError(domain: "HotUpdater", code: 0,
265
- userInfo: [NSLocalizedDescriptionKey: "Missing or empty 'bundleId'"])
276
+ let error = UpdateFailure.invalidResponse.error(description: "Missing or empty 'bundleId'")
266
277
  reject("MISSING_BUNDLE_ID", error.localizedDescription, error)
267
278
  return
268
279
  }
269
280
 
270
281
  guard let manifestFileHash = data["manifestFileHash"] as? String,
271
282
  !manifestFileHash.isEmpty else {
272
- let error = NSError(domain: "HotUpdater", code: 0,
273
- userInfo: [NSLocalizedDescriptionKey: "Missing manifest file hash"])
283
+ let error = UpdateFailure.invalidResponse.with(resource: .manifest)
284
+ .error(description: "Missing manifest file hash")
274
285
  reject("INVALID_MANIFEST", error.localizedDescription, error)
275
286
  return
276
287
  }
277
288
  let channel = data["channel"] as? String
278
289
  let manifestUrlString = data["manifestUrl"] as? String ?? ""
279
290
  guard let manifestUrl = URL(string: manifestUrlString) else {
280
- let error = NSError(domain: "HotUpdater", code: 0,
281
- userInfo: [NSLocalizedDescriptionKey: "Invalid 'manifestUrl' provided: \(manifestUrlString)"])
291
+ let error = UpdateFailure.invalidResponse.with(resource: .manifest).error(
292
+ description: "Invalid 'manifestUrl' provided: \(manifestUrlString)"
293
+ )
282
294
  reject("INVALID_FILE_URL", error.localizedDescription, error)
283
295
  return
284
296
  }
285
297
  let archiveUrl: URL?
286
298
  if let archiveUrlString = data["archiveUrl"] as? String {
287
299
  guard let parsedArchiveUrl = URL(string: archiveUrlString) else {
288
- let error = NSError(domain: "HotUpdater", code: 0,
289
- userInfo: [NSLocalizedDescriptionKey: "Invalid 'archiveUrl' provided: \(archiveUrlString)"])
300
+ let error = UpdateFailure.invalidResponse.with(resource: .archive).error(
301
+ description: "Invalid 'archiveUrl' provided: \(archiveUrlString)"
302
+ )
290
303
  reject("INVALID_FILE_URL", error.localizedDescription, error)
291
304
  return
292
305
  }
@@ -295,8 +308,8 @@ private func hotUpdaterGetMinBundleId() -> String {
295
308
  archiveUrl = nil
296
309
  }
297
310
  guard let assetsPayload = data["assets"] as? [String: [String: Any]] else {
298
- let error = NSError(domain: "HotUpdater", code: 0,
299
- userInfo: [NSLocalizedDescriptionKey: "Missing manifest assets"])
311
+ let error = UpdateFailure.invalidResponse.with(resource: .manifest)
312
+ .error(description: "Missing manifest assets")
300
313
  reject("INVALID_MANIFEST", error.localizedDescription, error)
301
314
  return
302
315
  }
@@ -344,11 +357,8 @@ private func hotUpdaterGetMinBundleId() -> String {
344
357
  }
345
358
  let selection = (data["selection"] as? [String: Any]).flatMap(Self.parseSelection)
346
359
  if let selection, !bundleStorage.stageReleaseSelection(selection) {
347
- let error = NSError(
348
- domain: "HotUpdater",
349
- code: 0,
350
- userInfo: [NSLocalizedDescriptionKey: "Release catalog selection is stale"]
351
- )
360
+ // Not an update failure, so the rejection carries no stage or reason.
361
+ let error = StaleReleaseSelectionError() as NSError
352
362
  reject("UNKNOWN_ERROR", error.localizedDescription, error)
353
363
  return
354
364
  }
@@ -383,7 +393,7 @@ private func hotUpdaterGetMinBundleId() -> String {
383
393
  // Return results on main thread for React Native bridge
384
394
  DispatchQueue.main.async {
385
395
  switch result {
386
- case .success:
396
+ case .success(let delivery):
387
397
  NSLog("[HotUpdaterImpl] Update successful for \(bundleId). Resolving promise.")
388
398
  if let channel, !channel.isEmpty {
389
399
  do {
@@ -396,12 +406,14 @@ private func hotUpdaterGetMinBundleId() -> String {
396
406
  NSLog("[HotUpdaterImpl] Failed to persist channel override: \(error)")
397
407
  }
398
408
  }
399
- resolve(true)
409
+ // { delivery: "patch" | "manifest" | "archive", patchFallback }
410
+ resolve(delivery.dictionary)
400
411
  case .failure(let error):
401
412
  NSLog("[HotUpdaterImpl] Update failed for \(bundleId) - Error: \(error)")
402
413
 
403
414
  let normalizedCode = HotUpdaterImpl.normalizeErrorCode(from: error)
404
- let nsError = error as NSError
415
+ // A classified failure adds stage, reason, and its details to userInfo.
416
+ let nsError = UpdateFailureError.rejectionError(for: error)
405
417
  reject(normalizedCode, nsError.localizedDescription, nsError)
406
418
  }
407
419
  }
@@ -536,6 +548,8 @@ private func hotUpdaterGetMinBundleId() -> String {
536
548
  * Rare or platform-specific codes are collapsed to UNKNOWN_ERROR to reduce surface area.
537
549
  */
538
550
  private static func normalizeErrorCode(from error: Error) -> String {
551
+ // Classification wraps an error without changing its code.
552
+ let error = UpdateFailureError.unwrap(error)
539
553
  let baseCode: String
540
554
 
541
555
  if let storageError = error as? BundleStorageError {
@@ -601,16 +615,21 @@ private func hotUpdaterGetMinBundleId() -> String {
601
615
  return bundleStorage.getInstallId()
602
616
  }
603
617
 
604
- public func getUserId() -> String? {
605
- return bundleStorage.getUserId()
606
- }
618
+ // MARK: - Client Plugin Storage
607
619
 
608
- public func getUsername() -> String? {
609
- return bundleStorage.getUsername()
620
+ /**
621
+ * Reads a value from the persistent key-value store for client plugins.
622
+ * @return The stored value or nil when the key has none
623
+ */
624
+ public func getStorageItem(_ key: String) -> String? {
625
+ return keyValueStorage.getItem(key)
610
626
  }
611
627
 
612
- public func setUser(_ userId: String?, username: String?) {
613
- bundleStorage.setUser(userId: userId, username: username)
628
+ /**
629
+ * Writes a value to the persistent key-value store; nil removes the key.
630
+ */
631
+ public func setStorageItem(_ key: String, value: String?) {
632
+ keyValueStorage.setItem(key, value: value)
614
633
  }
615
634
 
616
635
  /**
@@ -0,0 +1,71 @@
1
+ import Foundation
2
+
3
+ /// Persistent string key-value store for client plugins, kept in
4
+ /// `Application Support/HotUpdater/storage.json` outside device backups.
5
+ ///
6
+ /// Keys are opaque; JavaScript namespaces them per plugin. The file is read
7
+ /// into memory on first use, and every write replaces the whole file.
8
+ final class KeyValueStorageService {
9
+ static let storageFilename = "storage.json"
10
+
11
+ private let fileURL: URL
12
+ private let lock = NSLock()
13
+ private var items: [String: String]?
14
+
15
+ init(directory: URL) {
16
+ self.fileURL = directory.appendingPathComponent(Self.storageFilename)
17
+ }
18
+
19
+ func getItem(_ key: String) -> String? {
20
+ lock.lock()
21
+ defer { lock.unlock() }
22
+ return loadedItems()[key]
23
+ }
24
+
25
+ /// Stores `value` under `key`, or removes the key when `value` is nil.
26
+ /// Returns false when the file could not be written.
27
+ @discardableResult
28
+ func setItem(_ key: String, value: String?) -> Bool {
29
+ lock.lock()
30
+ defer { lock.unlock() }
31
+
32
+ var updatedItems = loadedItems()
33
+ updatedItems[key] = value
34
+ // Memory stays the source of truth for this process, so the next
35
+ // successful write also persists a value whose own write failed.
36
+ items = updatedItems
37
+
38
+ do {
39
+ let data = try JSONSerialization.data(
40
+ withJSONObject: updatedItems,
41
+ options: [.sortedKeys]
42
+ )
43
+ try NoBackupStorage.write(data, to: fileURL)
44
+ return true
45
+ } catch {
46
+ NSLog("[KeyValueStorage] Failed to write \(fileURL.path): \(error)")
47
+ return false
48
+ }
49
+ }
50
+
51
+ private func loadedItems() -> [String: String] {
52
+ if let items {
53
+ return items
54
+ }
55
+ let loadedItems = Self.readItems(from: fileURL)
56
+ items = loadedItems
57
+ return loadedItems
58
+ }
59
+
60
+ /// A missing, unreadable, or corrupt file reads as an empty store.
61
+ private static func readItems(from file: URL) -> [String: String] {
62
+ guard let data = try? Data(contentsOf: file) else {
63
+ return [:]
64
+ }
65
+ guard let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
66
+ NSLog("[KeyValueStorage] Ignoring unreadable store at \(file.path)")
67
+ return [:]
68
+ }
69
+ return object.compactMapValues { $0 as? String }
70
+ }
71
+ }
@@ -23,9 +23,108 @@ protocol DownloadService {
23
23
  }
24
24
 
25
25
 
26
- enum DownloadError: Error {
26
+ enum DownloadError: Error, Equatable {
27
27
  case incompleteDownload(expected: Int64, actual: Int64)
28
28
  case invalidContentLength
29
+ /// The server answered with a status outside 200-299. `originCode` is the
30
+ /// `<Code>` of a storage origin's XML error body, when it has one.
31
+ case httpStatus(Int, originCode: String? = nil)
32
+
33
+ /// How much of an error body is searched for a storage origin's code.
34
+ static let maximumErrorBodyPrefixByteCount = 4 * 1024
35
+ private static let maximumOriginCodeLength = 64
36
+
37
+ /// The failure for an HTTP response outside 200-299, or nil for a
38
+ /// successful or non-HTTP response. `body` is the saved response body.
39
+ static func httpStatusError(for response: URLResponse?, body: URL?) -> DownloadError? {
40
+ guard let httpResponse = response as? HTTPURLResponse,
41
+ !(200..<300).contains(httpResponse.statusCode) else {
42
+ return nil
43
+ }
44
+ let originCode = body.flatMap { body -> String? in
45
+ guard let handle = try? FileHandle(forReadingFrom: body) else {
46
+ return nil
47
+ }
48
+ defer { try? handle.close() }
49
+ let prefix = try? FileUtilities.readUpToCount(
50
+ from: handle,
51
+ count: maximumErrorBodyPrefixByteCount
52
+ )
53
+ return prefix.flatMap { storageOriginErrorCode(in: $0) }
54
+ }
55
+ return .httpStatus(httpResponse.statusCode, originCode: originCode)
56
+ }
57
+
58
+ /// The `<Code>` of an S3, R2, or GCS style `<Error>` document in the
59
+ /// first 4 KB of `body`, such as "AccessDenied". Nothing else from the
60
+ /// body is kept, and a code must be 1-64 characters of [A-Za-z0-9._-].
61
+ static func storageOriginErrorCode(in body: Data) -> String? {
62
+ let text = String(decoding: body.prefix(maximumErrorBodyPrefixByteCount), as: UTF8.self)
63
+ guard let errorElement = text.range(of: "<Error"),
64
+ let codeStart = text.range(of: "<Code>", range: errorElement.upperBound..<text.endIndex),
65
+ let codeEnd = text.range(of: "</Code>", range: codeStart.upperBound..<text.endIndex) else {
66
+ return nil
67
+ }
68
+ let code = text[codeStart.upperBound..<codeEnd.lowerBound]
69
+ .trimmingCharacters(in: .whitespacesAndNewlines)
70
+ guard (1...maximumOriginCodeLength).contains(code.unicodeScalars.count),
71
+ code.unicodeScalars.allSatisfy(isOriginCodeCharacter) else {
72
+ return nil
73
+ }
74
+ return code
75
+ }
76
+
77
+ private static func isOriginCodeCharacter(_ scalar: Unicode.Scalar) -> Bool {
78
+ switch scalar {
79
+ case "A"..."Z", "a"..."z", "0"..."9", ".", "_", "-":
80
+ return true
81
+ default:
82
+ return false
83
+ }
84
+ }
85
+ }
86
+
87
+ extension DownloadError {
88
+ /// Attempts per download that retries, the first one included.
89
+ static let maximumAttempts = 3
90
+
91
+ /// Whether a failed download may succeed if tried again: a network
92
+ /// failure or timeout, a body that ended early, or a 408, 429, or 5xx
93
+ /// answer. Another 4xx, a TLS failure, a cancelled task, and a local file
94
+ /// failure would fail the same way again.
95
+ static func isRetryable(_ error: Error) -> Bool {
96
+ if let downloadError = error as? DownloadError {
97
+ switch downloadError {
98
+ case .httpStatus(let status, _):
99
+ return status == 408 || status == 429 || status >= 500
100
+ case .incompleteDownload:
101
+ return true
102
+ case .invalidContentLength:
103
+ return false
104
+ }
105
+ }
106
+ let nsError = error as NSError
107
+ guard nsError.domain == NSURLErrorDomain else {
108
+ return false
109
+ }
110
+ switch URLError.Code(rawValue: nsError.code) {
111
+ case .timedOut,
112
+ .cannotFindHost,
113
+ .cannotConnectToHost,
114
+ .networkConnectionLost,
115
+ .dnsLookupFailed,
116
+ .notConnectedToInternet,
117
+ .cannotLoadFromNetwork,
118
+ .callIsActive,
119
+ .dataNotAllowed,
120
+ .internationalRoamingOff,
121
+ .backgroundSessionWasDisconnected,
122
+ .badServerResponse:
123
+ return true
124
+ default:
125
+ return false
126
+ }
127
+ }
29
128
  }
30
129
 
31
130
  class URLSessionDownloadService: NSObject, DownloadService {
@@ -107,6 +206,14 @@ extension URLSessionDownloadService: URLSessionDownloadDelegate {
107
206
  return
108
207
  }
109
208
 
209
+ // URLSession saves error bodies too, so reject them before they reach verification.
210
+ if let httpError = DownloadError.httpStatusError(for: downloadTask.response, body: location) {
211
+ NSLog("[DownloadService] Download failed: \(httpError)")
212
+ try? FileManager.default.removeItem(at: location)
213
+ completion?(.failure(httpError))
214
+ return
215
+ }
216
+
110
217
  // Verify file size
111
218
  let expectedSize = downloadTask.response?.expectedContentLength ?? -1
112
219
  let actualSize: Int64?