@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
@@ -0,0 +1,297 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ import type {
4
+ HotUpdaterClientContext,
5
+ HotUpdaterClientPlugin,
6
+ } from "./clientPlugin";
7
+
8
+ vi.mock("react-native", () => ({
9
+ Platform: { OS: "android" },
10
+ }));
11
+
12
+ const mocks = vi.hoisted(() => {
13
+ Reflect.set(globalThis, "HotUpdater", { SDK_VERSION: "test-sdk-version" });
14
+ return {
15
+ getAppVersion: vi.fn<() => string | null>(() => "1.2.3"),
16
+ getBundleId: vi.fn(() => "bundle-id"),
17
+ getChannel: vi.fn(() => "production"),
18
+ getCohort: vi.fn(() => "123"),
19
+ getFingerprintHash: vi.fn<() => string | null>(() => null),
20
+ getInstallId: vi.fn(() => "install-id"),
21
+ getStorageItem: vi.fn<(key: string) => string | null>(() => null),
22
+ setStorageItem: vi.fn<(key: string, value: string | null) => void>(),
23
+ };
24
+ });
25
+
26
+ vi.mock("./native", () => mocks);
27
+
28
+ const importHost = () => import("./pluginHost");
29
+
30
+ const capture = (id = "example") => {
31
+ let context!: HotUpdaterClientContext;
32
+ const setup = vi.fn((pluginContext: HotUpdaterClientContext) => {
33
+ context = pluginContext;
34
+ });
35
+ const plugin: HotUpdaterClientPlugin = { id, setup };
36
+ return { plugin, setup, context: () => context };
37
+ };
38
+
39
+ describe("client plugin host", () => {
40
+ beforeEach(() => {
41
+ vi.resetModules();
42
+ for (const mock of Object.values(mocks)) mock.mockClear();
43
+ mocks.getStorageItem.mockReturnValue(null);
44
+ });
45
+
46
+ afterEach(() => {
47
+ vi.useRealTimers();
48
+ vi.unstubAllGlobals();
49
+ vi.restoreAllMocks();
50
+ });
51
+
52
+ it("throws at configuration for two plugins with one id", async () => {
53
+ const { configurePlugins } = await importHost();
54
+
55
+ expect(() =>
56
+ configurePlugins([capture("same").plugin, capture("same").plugin], {
57
+ baseURL: "https://updates.example.com",
58
+ }),
59
+ ).toThrow('Two plugins use the id "same"');
60
+ });
61
+
62
+ it("sets a plugin up once and stops calling a plugin left out", async () => {
63
+ const { configurePlugins, emitPluginHook } = await importHost();
64
+ const onAppReady = vi.fn();
65
+ const plugin: HotUpdaterClientPlugin = {
66
+ id: "example",
67
+ setup: vi.fn(() => ({ onAppReady })),
68
+ };
69
+ const config = { baseURL: "https://updates.example.com" };
70
+
71
+ configurePlugins([plugin], config);
72
+ configurePlugins([plugin], config);
73
+ emitPluginHook("onAppReady", () => ({
74
+ status: "UNCHANGED",
75
+ channel: "production",
76
+ bundleId: "bundle-id",
77
+ releaseId: null,
78
+ previousProcessExit: null,
79
+ }));
80
+ configurePlugins([], config);
81
+ emitPluginHook("onAppReady", () => ({
82
+ status: "UNCHANGED",
83
+ channel: "production",
84
+ bundleId: "bundle-id",
85
+ releaseId: null,
86
+ previousProcessExit: null,
87
+ }));
88
+
89
+ expect(plugin.setup).toHaveBeenCalledOnce();
90
+ expect(onAppReady).toHaveBeenCalledOnce();
91
+ });
92
+
93
+ it("warns without onError when setup throws, and keeps other plugins", async () => {
94
+ const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
95
+ const { configurePlugins } = await importHost();
96
+ const healthy = capture("healthy");
97
+
98
+ configurePlugins(
99
+ [
100
+ {
101
+ id: "broken",
102
+ setup: () => {
103
+ throw new Error("setup failed");
104
+ },
105
+ },
106
+ healthy.plugin,
107
+ ],
108
+ { baseURL: "https://updates.example.com" },
109
+ );
110
+
111
+ expect(healthy.setup).toHaveBeenCalledOnce();
112
+ expect(warn).toHaveBeenCalledWith(
113
+ '[HotUpdater] Plugin "broken" failed in setup',
114
+ expect.objectContaining({ message: "setup failed" }),
115
+ );
116
+ });
117
+
118
+ it("never waits for a hook", async () => {
119
+ const { configurePlugins, emitPluginHook } = await importHost();
120
+ const onUpdateCheck = vi.fn(() => new Promise<void>(() => {}));
121
+ configurePlugins([{ id: "slow", setup: () => ({ onUpdateCheck }) }], {
122
+ baseURL: "https://updates.example.com",
123
+ });
124
+
125
+ const result = emitPluginHook("onUpdateCheck", () => ({
126
+ status: "UNCHANGED",
127
+ channel: "production",
128
+ bundleId: "bundle-id",
129
+ releaseId: null,
130
+ previousReleaseId: null,
131
+ }));
132
+
133
+ expect(result).toBeUndefined();
134
+ expect(onUpdateCheck).toHaveBeenCalledOnce();
135
+ });
136
+
137
+ it("builds an event only when a plugin listens", async () => {
138
+ const { configurePlugins, emitPluginHook } = await importHost();
139
+ configurePlugins([capture().plugin], {
140
+ baseURL: "https://updates.example.com",
141
+ });
142
+ const createPayload = vi.fn(() => null);
143
+
144
+ emitPluginHook("onUpdateError", createPayload);
145
+
146
+ expect(createPayload).not.toHaveBeenCalled();
147
+ });
148
+
149
+ it("exposes the app's identity and environment", async () => {
150
+ vi.stubGlobal("__DEV__", true);
151
+ vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
152
+ const { configurePlugins } = await importHost();
153
+ const plugin = capture();
154
+ configurePlugins([plugin.plugin], {
155
+ baseURL: "https://updates.example.com",
156
+ });
157
+ const context = plugin.context();
158
+
159
+ expect({
160
+ appVersion: context.appVersion,
161
+ bundleId: context.getBundleId(),
162
+ channel: context.getChannel(),
163
+ cohort: context.getCohort(),
164
+ fingerprintHash: context.getFingerprintHash(),
165
+ installId: context.installId,
166
+ isDebugBuild: context.isDebugBuild,
167
+ now: context.now(),
168
+ platform: context.platform,
169
+ sdkVersion: context.sdkVersion,
170
+ }).toEqual({
171
+ appVersion: "1.2.3",
172
+ bundleId: "bundle-id",
173
+ channel: "production",
174
+ cohort: "123",
175
+ fingerprintHash: null,
176
+ installId: "install-id",
177
+ isDebugBuild: true,
178
+ now: 1_700_000_000_000,
179
+ platform: "android",
180
+ sdkVersion: "test-sdk-version",
181
+ });
182
+ });
183
+
184
+ describe("fetch", () => {
185
+ it("requests a path under the resolved baseURL with the SDK's headers", async () => {
186
+ const fetchMock = vi.fn<typeof fetch>(
187
+ async () => new Response(null, { status: 204 }),
188
+ );
189
+ vi.stubGlobal("fetch", fetchMock);
190
+ const { configurePlugins } = await importHost();
191
+ const plugin = capture();
192
+ configurePlugins([plugin.plugin], {
193
+ baseURL: async () => "https://updates.example.com/hot-updater/",
194
+ requestHeaders: { "x-api-key": "client-key", "X-Shared": "sdk" },
195
+ });
196
+
197
+ await plugin.context().fetch("/events", {
198
+ headers: { "Content-Type": "application/json", "X-Shared": "plugin" },
199
+ method: "POST",
200
+ });
201
+
202
+ const [url, init] = fetchMock.mock.calls[0]!;
203
+ expect(url).toBe("https://updates.example.com/hot-updater/events");
204
+ expect(init?.method).toBe("POST");
205
+ const headers = new Headers(init?.headers);
206
+ expect(headers.get("x-api-key")).toBe("client-key");
207
+ expect(headers.get("content-type")).toBe("application/json");
208
+ expect(headers.get("x-shared")).toBe("plugin");
209
+ });
210
+
211
+ it("rejects with Request timed out after the SDK's timeout", async () => {
212
+ vi.useFakeTimers();
213
+ vi.stubGlobal(
214
+ "fetch",
215
+ vi.fn(
216
+ (_input: unknown, init?: RequestInit) =>
217
+ new Promise((_resolve, reject) => {
218
+ init?.signal?.addEventListener("abort", () => {
219
+ reject(
220
+ Object.assign(new Error("aborted"), { name: "AbortError" }),
221
+ );
222
+ });
223
+ }),
224
+ ),
225
+ );
226
+ const { configurePlugins } = await importHost();
227
+ const plugin = capture();
228
+ configurePlugins([plugin.plugin], {
229
+ baseURL: "https://updates.example.com",
230
+ requestTimeout: 2_000,
231
+ });
232
+
233
+ const request = plugin.context().fetch("events");
234
+ const settled = expect(request).rejects.toThrow("Request timed out");
235
+ await vi.advanceTimersByTimeAsync(2_000);
236
+ await settled;
237
+ });
238
+
239
+ it("refuses a URL outside baseURL", async () => {
240
+ const { configurePlugins } = await importHost();
241
+ const plugin = capture();
242
+ configurePlugins([plugin.plugin], {
243
+ baseURL: "https://updates.example.com",
244
+ });
245
+
246
+ await expect(
247
+ plugin.context().fetch("https://elsewhere.example.com/events"),
248
+ ).rejects.toThrow("relative to baseURL");
249
+ });
250
+ });
251
+
252
+ describe("storage", () => {
253
+ it("scopes keys to the plugin and reads its own writes", async () => {
254
+ mocks.getStorageItem.mockReturnValue("from-native");
255
+ const { configurePlugins } = await importHost();
256
+ const plugin = capture("example");
257
+ configurePlugins([plugin.plugin], {
258
+ baseURL: "https://updates.example.com",
259
+ });
260
+ const { storage } = plugin.context();
261
+
262
+ expect(storage.get("state")).toBe("from-native");
263
+ expect(storage.get("state")).toBe("from-native");
264
+ storage.set("state", "next");
265
+ expect(storage.get("state")).toBe("next");
266
+ storage.set("state", null);
267
+ expect(storage.get("state")).toBeNull();
268
+
269
+ expect(mocks.getStorageItem).toHaveBeenCalledOnce();
270
+ expect(mocks.getStorageItem).toHaveBeenCalledWith(
271
+ "plugins/example/state",
272
+ );
273
+ expect(mocks.setStorageItem.mock.calls).toEqual([
274
+ ["plugins/example/state", "next"],
275
+ ["plugins/example/state", null],
276
+ ]);
277
+ });
278
+
279
+ it("refuses empty keys, non-string values, and values over 64 KB", async () => {
280
+ const { configurePlugins } = await importHost();
281
+ const plugin = capture();
282
+ configurePlugins([plugin.plugin], {
283
+ baseURL: "https://updates.example.com",
284
+ });
285
+ const { storage } = plugin.context();
286
+
287
+ expect(() => storage.get("")).toThrow("non-empty strings");
288
+ expect(() => storage.set("key", 1 as unknown as string)).toThrow(
289
+ "strings or null",
290
+ );
291
+ expect(() => storage.set("key", "x".repeat(64 * 1024 + 1))).toThrow(
292
+ "at most 64 KB",
293
+ );
294
+ expect(mocks.setStorageItem).not.toHaveBeenCalled();
295
+ });
296
+ });
297
+ });
@@ -0,0 +1,262 @@
1
+ import { Platform } from "react-native";
2
+
3
+ import { resolveBaseURL } from "./baseURL";
4
+ import type {
5
+ HotUpdaterClientContext,
6
+ HotUpdaterClientHooks,
7
+ HotUpdaterClientPlugin,
8
+ HotUpdaterClientStorage,
9
+ } from "./clientPlugin";
10
+ import {
11
+ getAppVersion,
12
+ getBundleId,
13
+ getChannel,
14
+ getCohort,
15
+ getFingerprintHash,
16
+ getInstallId,
17
+ getStorageItem,
18
+ setStorageItem,
19
+ } from "./native";
20
+ import { HOT_UPDATER_SDK_VERSION } from "./sdkVersion";
21
+ import type { HotUpdaterBaseURL } from "./types";
22
+
23
+ type HookName = keyof HotUpdaterClientHooks;
24
+ type HookPayload<K extends HookName> = Parameters<
25
+ NonNullable<HotUpdaterClientHooks[K]>
26
+ >[0];
27
+
28
+ /** The init or wrap settings plugins see through their context. */
29
+ export interface PluginHostConfig {
30
+ readonly baseURL: HotUpdaterBaseURL;
31
+ readonly requestHeaders?: Record<string, string>;
32
+ readonly requestTimeout?: number;
33
+ readonly onError?: (error: unknown) => void;
34
+ }
35
+
36
+ /** Storage values stay small, since native reads them on the JS thread. */
37
+ const MAX_STORAGE_VALUE_LENGTH = 64 * 1024;
38
+
39
+ let config: PluginHostConfig | null = null;
40
+ /** Hooks by plugin, so a plugin configured again is not set up twice. */
41
+ const setUpPlugins = new WeakMap<
42
+ HotUpdaterClientPlugin,
43
+ HotUpdaterClientHooks
44
+ >();
45
+ let activePlugins: readonly {
46
+ readonly plugin: HotUpdaterClientPlugin;
47
+ readonly hooks: HotUpdaterClientHooks;
48
+ }[] = [];
49
+ /** What plugins read and wrote, so a read follows a write before native does. */
50
+ const storageValues = new Map<string, string | null>();
51
+
52
+ const reportHookError = (message: string, error: unknown) => {
53
+ const report = new Error(`[HotUpdater] ${message}`, { cause: error });
54
+ try {
55
+ if (config?.onError) {
56
+ config.onError(report);
57
+ return;
58
+ }
59
+ } catch {
60
+ // A throwing onError must not break the update path either.
61
+ }
62
+ console.warn(report.message, error);
63
+ };
64
+
65
+ const fetchFromServer = async (
66
+ path: string,
67
+ init: RequestInit = {},
68
+ ): Promise<Response> => {
69
+ if (config === null) {
70
+ throw new Error("[HotUpdater] Plugins can fetch only after init or wrap.");
71
+ }
72
+ if (/^[a-z][a-z\d+.-]*:/i.test(path) || path.startsWith("//")) {
73
+ throw new TypeError(
74
+ "[HotUpdater] Plugin fetch takes a path relative to baseURL.",
75
+ );
76
+ }
77
+ const { requestHeaders, requestTimeout } = config;
78
+ const url = `${await resolveBaseURL(config.baseURL)}/${path.replace(/^\/+/, "")}`;
79
+ const headers = new Headers(requestHeaders);
80
+ new Headers(init.headers).forEach((value, name) => {
81
+ headers.set(name, value);
82
+ });
83
+
84
+ const controller = new AbortController();
85
+ const callerSignal = init.signal;
86
+ const abortFromCaller = () => controller.abort();
87
+ if (callerSignal?.aborted) controller.abort();
88
+ callerSignal?.addEventListener("abort", abortFromCaller);
89
+ const timeoutId = setTimeout(
90
+ () => controller.abort(),
91
+ requestTimeout ?? 5000,
92
+ );
93
+
94
+ try {
95
+ return await fetch(url, { ...init, headers, signal: controller.signal });
96
+ } catch (error: unknown) {
97
+ if (
98
+ error instanceof Error &&
99
+ error.name === "AbortError" &&
100
+ !callerSignal?.aborted
101
+ ) {
102
+ throw new Error("Request timed out");
103
+ }
104
+ throw error;
105
+ } finally {
106
+ clearTimeout(timeoutId);
107
+ callerSignal?.removeEventListener("abort", abortFromCaller);
108
+ }
109
+ };
110
+
111
+ const createStorage = (pluginId: string): HotUpdaterClientStorage => {
112
+ const scopedKey = (key: string) => {
113
+ if (typeof key !== "string" || key.length === 0) {
114
+ throw new TypeError(
115
+ "[HotUpdater] Plugin storage keys are non-empty strings.",
116
+ );
117
+ }
118
+ // Encoded, so no id and key pair can name another plugin's key.
119
+ return `plugins/${encodeURIComponent(pluginId)}/${key}`;
120
+ };
121
+
122
+ return {
123
+ get: (key) => {
124
+ const storageKey = scopedKey(key);
125
+ const cached = storageValues.get(storageKey);
126
+ if (cached !== undefined) return cached;
127
+ const value = getStorageItem(storageKey);
128
+ storageValues.set(storageKey, value);
129
+ return value;
130
+ },
131
+ set: (key, value) => {
132
+ const storageKey = scopedKey(key);
133
+ if (value !== null && typeof value !== "string") {
134
+ throw new TypeError(
135
+ "[HotUpdater] Plugin storage values are strings or null.",
136
+ );
137
+ }
138
+ if (value !== null && value.length > MAX_STORAGE_VALUE_LENGTH) {
139
+ throw new RangeError(
140
+ "[HotUpdater] Plugin storage values are at most 64 KB.",
141
+ );
142
+ }
143
+ setStorageItem(storageKey, value);
144
+ storageValues.set(storageKey, value);
145
+ },
146
+ };
147
+ };
148
+
149
+ const createContext = (pluginId: string): HotUpdaterClientContext => ({
150
+ fetch: fetchFromServer,
151
+ get installId() {
152
+ return getInstallId();
153
+ },
154
+ platform: Platform.OS === "android" ? "android" : "ios",
155
+ get appVersion() {
156
+ return getAppVersion();
157
+ },
158
+ get sdkVersion() {
159
+ return HOT_UPDATER_SDK_VERSION;
160
+ },
161
+ isDebugBuild: typeof __DEV__ !== "undefined" && __DEV__,
162
+ getBundleId,
163
+ getChannel,
164
+ getCohort,
165
+ getFingerprintHash,
166
+ storage: createStorage(pluginId),
167
+ now: () => Date.now(),
168
+ });
169
+
170
+ /**
171
+ * Sets up the plugins of an init or wrap call. A plugin passed to an earlier
172
+ * call keeps its hooks, and a plugin left out stops receiving events.
173
+ */
174
+ export const configurePlugins = (
175
+ plugins: readonly HotUpdaterClientPlugin[] | undefined,
176
+ nextConfig: PluginHostConfig,
177
+ ): void => {
178
+ const ids = new Set<string>();
179
+ for (const plugin of plugins ?? []) {
180
+ if (typeof plugin.id !== "string" || plugin.id.length === 0) {
181
+ throw new Error("[HotUpdater] A plugin id must be a non-empty string.");
182
+ }
183
+ if (ids.has(plugin.id)) {
184
+ throw new Error(
185
+ `[HotUpdater] Two plugins use the id "${plugin.id}". Plugin ids must be unique.`,
186
+ );
187
+ }
188
+ ids.add(plugin.id);
189
+ }
190
+
191
+ config = nextConfig;
192
+ activePlugins = (plugins ?? []).map((plugin) => {
193
+ let hooks = setUpPlugins.get(plugin);
194
+ if (hooks === undefined) {
195
+ try {
196
+ hooks = plugin.setup(createContext(plugin.id)) ?? {};
197
+ } catch (error) {
198
+ reportHookError(`Plugin "${plugin.id}" failed in setup`, error);
199
+ hooks = {};
200
+ }
201
+ setUpPlugins.set(plugin, hooks);
202
+ }
203
+ return { plugin, hooks };
204
+ });
205
+ };
206
+
207
+ /** Whether any plugin listens to this hook, so callers build events only when one does. */
208
+ export const hasPluginHook = (name: HookName): boolean =>
209
+ activePlugins.some(({ hooks }) => typeof hooks[name] === "function");
210
+
211
+ /**
212
+ * Builds a hook's event when a plugin listens. A failure to build it is
213
+ * reported, not thrown, since reporting never changes an update.
214
+ */
215
+ export const buildPluginEvent = <K extends HookName>(
216
+ name: K,
217
+ createPayload: () => HookPayload<K> | null,
218
+ ): HookPayload<K> | null => {
219
+ if (!hasPluginHook(name)) return null;
220
+ try {
221
+ return createPayload();
222
+ } catch (error) {
223
+ reportHookError(`Failed to build the ${name} event for plugins`, error);
224
+ return null;
225
+ }
226
+ };
227
+
228
+ /** Calls a hook on every plugin that listens, without waiting for any. */
229
+ export const dispatchPluginHook = <K extends HookName>(
230
+ name: K,
231
+ payload: HookPayload<K>,
232
+ ): void => {
233
+ for (const { plugin, hooks } of activePlugins) {
234
+ const hook = hooks[name] as
235
+ | ((payload: HookPayload<K>) => unknown)
236
+ | undefined;
237
+ if (typeof hook !== "function") continue;
238
+ try {
239
+ const result = hook.call(hooks, payload);
240
+ if (
241
+ result !== null &&
242
+ typeof result === "object" &&
243
+ typeof (result as PromiseLike<unknown>).then === "function"
244
+ ) {
245
+ void Promise.resolve(result).catch((error: unknown) => {
246
+ reportHookError(`Plugin "${plugin.id}" failed in ${name}`, error);
247
+ });
248
+ }
249
+ } catch (error) {
250
+ reportHookError(`Plugin "${plugin.id}" failed in ${name}`, error);
251
+ }
252
+ }
253
+ };
254
+
255
+ /** Builds a hook's event when a plugin listens and calls the hook right away. */
256
+ export const emitPluginHook = <K extends HookName>(
257
+ name: K,
258
+ createPayload: () => HookPayload<K> | null,
259
+ ): void => {
260
+ const payload = buildPluginEvent(name, createPayload);
261
+ if (payload !== null) dispatchPluginHook(name, payload);
262
+ };
@@ -0,0 +1,60 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { isUUIDv7 } from "@hot-updater/core";
4
+ import { afterEach, describe, expect, it, vi } from "vitest";
5
+
6
+ import { createKeyedUUIDv7, createUUIDv7, sha256 } from "./eventId";
7
+
8
+ describe("createUUIDv7", () => {
9
+ afterEach(() => {
10
+ vi.useRealTimers();
11
+ vi.unstubAllGlobals();
12
+ });
13
+
14
+ it.each([
15
+ { label: "Web Crypto", crypto: globalThis.crypto },
16
+ { label: "no Web Crypto, as in React Native", crypto: undefined },
17
+ ])("leads with the 48-bit millisecond time using $label", ({ crypto }) => {
18
+ vi.useFakeTimers({ now: 0x0192_3456_789a });
19
+ vi.stubGlobal("crypto", crypto);
20
+
21
+ const id = createUUIDv7();
22
+
23
+ expect(isUUIDv7(id)).toBe(true);
24
+ expect(id.startsWith("01923456-789a-7")).toBe(true);
25
+ expect(createUUIDv7()).not.toBe(id);
26
+ });
27
+ });
28
+
29
+ describe("sha256", () => {
30
+ it.each([
31
+ "",
32
+ "abc",
33
+ "a".repeat(55),
34
+ "a".repeat(56),
35
+ "a".repeat(64),
36
+ "a".repeat(1_000),
37
+ "install-id\n2026-09-30\ndownload hash_mismatch bundle-id",
38
+ "한글 및 이모지 😀",
39
+ ])("matches Node's SHA-256 for %j", (value) => {
40
+ expect(Buffer.from(sha256(value)).toString("hex")).toBe(
41
+ createHash("sha256").update(value, "utf8").digest("hex"),
42
+ );
43
+ });
44
+ });
45
+
46
+ describe("createKeyedUUIDv7", () => {
47
+ it("gives one UUIDv7 per key and time, led by that time", () => {
48
+ const midnight = Date.UTC(2026, 8, 30);
49
+
50
+ const id = createKeyedUUIDv7("install-id\nfailure", midnight);
51
+
52
+ expect(isUUIDv7(id)).toBe(true);
53
+ expect(parseInt(id.replace(/-/g, "").slice(0, 12), 16)).toBe(midnight);
54
+ expect(createKeyedUUIDv7("install-id\nfailure", midnight)).toBe(id);
55
+ expect(createKeyedUUIDv7("install-id\nother", midnight)).not.toBe(id);
56
+ expect(
57
+ createKeyedUUIDv7("install-id\nfailure", midnight + 86_400_000),
58
+ ).not.toBe(id);
59
+ });
60
+ });