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

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 (240) hide show
  1. package/lib/commonjs/checkForUpdate.js +7 -9
  2. package/lib/commonjs/checkForUpdate.js.map +1 -1
  3. package/lib/commonjs/clientPlugin.js +7 -43
  4. package/lib/commonjs/clientPlugin.js.map +1 -1
  5. package/lib/commonjs/httpClient.js +7 -9
  6. package/lib/commonjs/httpClient.js.map +1 -1
  7. package/lib/commonjs/index.js +10 -0
  8. package/lib/commonjs/index.js.map +1 -1
  9. package/lib/commonjs/native.js +4 -4
  10. package/lib/commonjs/native.js.map +1 -1
  11. package/lib/commonjs/pluginHost.js +26 -190
  12. package/lib/commonjs/pluginHost.js.map +1 -1
  13. package/lib/commonjs/releaseCatalogCache.js +7 -59
  14. package/lib/commonjs/releaseCatalogCache.js.map +1 -1
  15. package/lib/commonjs/sdkVersion.js +1 -1
  16. package/lib/module/checkForUpdate.js +1 -3
  17. package/lib/module/checkForUpdate.js.map +1 -1
  18. package/lib/module/clientPlugin.js +1 -41
  19. package/lib/module/clientPlugin.js.map +1 -1
  20. package/lib/module/httpClient.js +1 -3
  21. package/lib/module/httpClient.js.map +1 -1
  22. package/lib/module/index.js +2 -0
  23. package/lib/module/index.js.map +1 -1
  24. package/lib/module/native.js +1 -1
  25. package/lib/module/native.js.map +1 -1
  26. package/lib/module/pluginHost.js +22 -187
  27. package/lib/module/pluginHost.js.map +1 -1
  28. package/lib/module/releaseCatalogCache.js +3 -54
  29. package/lib/module/releaseCatalogCache.js.map +1 -1
  30. package/lib/module/sdkVersion.js +1 -1
  31. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/clientPlugin.d.ts +1 -184
  33. package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/httpClient.d.ts +1 -2
  35. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/index.d.ts +1 -0
  37. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  38. package/lib/typescript/commonjs/native.d.ts +1 -1
  39. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/pluginHost.d.ts +3 -28
  41. package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/releaseCatalogCache.d.ts +1 -13
  43. package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  45. package/lib/typescript/commonjs/types.d.ts +1 -1
  46. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  47. package/lib/typescript/commonjs/wrap.types.d.ts +2 -2
  48. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  49. package/lib/typescript/module/clientPlugin.d.ts +1 -184
  50. package/lib/typescript/module/clientPlugin.d.ts.map +1 -1
  51. package/lib/typescript/module/httpClient.d.ts +1 -2
  52. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  53. package/lib/typescript/module/index.d.ts +1 -0
  54. package/lib/typescript/module/index.d.ts.map +1 -1
  55. package/lib/typescript/module/native.d.ts +1 -1
  56. package/lib/typescript/module/native.d.ts.map +1 -1
  57. package/lib/typescript/module/pluginHost.d.ts +3 -28
  58. package/lib/typescript/module/pluginHost.d.ts.map +1 -1
  59. package/lib/typescript/module/releaseCatalogCache.d.ts +1 -13
  60. package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
  61. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  62. package/lib/typescript/module/types.d.ts +1 -1
  63. package/lib/typescript/module/types.d.ts.map +1 -1
  64. package/lib/typescript/module/wrap.types.d.ts +2 -2
  65. package/package.json +7 -16
  66. package/src/checkForUpdate.ts +4 -6
  67. package/src/clientPlugin.ts +17 -231
  68. package/src/httpClient.ts +4 -4
  69. package/src/index.ts +7 -0
  70. package/src/native.ts +1 -1
  71. package/src/pluginHost.ts +24 -241
  72. package/src/releaseCatalogCache.ts +7 -136
  73. package/src/sdkVersion.ts +1 -1
  74. package/src/types.ts +1 -1
  75. package/src/wrap.types.ts +2 -2
  76. package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
  77. package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -2236
  78. package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +0 -220
  79. package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -264
  80. package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +0 -97
  81. package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +0 -71
  82. package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +0 -106
  83. package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
  84. package/android/src/test/java/com/hotupdater/NoBackupContext.kt +0 -11
  85. package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -397
  86. package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +0 -44
  87. package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +0 -57
  88. package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
  89. package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
  90. package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +0 -206
  91. package/lib/commonjs/appReady.spec.js +0 -360
  92. package/lib/commonjs/appReady.spec.js.map +0 -1
  93. package/lib/commonjs/appReady.test-utils.js +0 -25
  94. package/lib/commonjs/appReady.test-utils.js.map +0 -1
  95. package/lib/commonjs/baseURL.js +0 -16
  96. package/lib/commonjs/baseURL.js.map +0 -1
  97. package/lib/commonjs/catalogCacheNative.spec.js +0 -31
  98. package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
  99. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -936
  100. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  101. package/lib/commonjs/entryImportGraph.spec.js +0 -50
  102. package/lib/commonjs/entryImportGraph.spec.js.map +0 -1
  103. package/lib/commonjs/httpClient.spec.js +0 -171
  104. package/lib/commonjs/httpClient.spec.js.map +0 -1
  105. package/lib/commonjs/index.spec.js +0 -395
  106. package/lib/commonjs/index.spec.js.map +0 -1
  107. package/lib/commonjs/iosInterop.spec.js +0 -18
  108. package/lib/commonjs/iosInterop.spec.js.map +0 -1
  109. package/lib/commonjs/native.spec.js +0 -910
  110. package/lib/commonjs/native.spec.js.map +0 -1
  111. package/lib/commonjs/pluginHost.spec.js +0 -292
  112. package/lib/commonjs/pluginHost.spec.js.map +0 -1
  113. package/lib/commonjs/plugins/insights/eventId.js +0 -129
  114. package/lib/commonjs/plugins/insights/eventId.js.map +0 -1
  115. package/lib/commonjs/plugins/insights/eventId.spec.js +0 -47
  116. package/lib/commonjs/plugins/insights/eventId.spec.js.map +0 -1
  117. package/lib/commonjs/plugins/insights/index.js +0 -272
  118. package/lib/commonjs/plugins/insights/index.js.map +0 -1
  119. package/lib/commonjs/plugins/insights/insights.spec.js +0 -640
  120. package/lib/commonjs/plugins/insights/insights.spec.js.map +0 -1
  121. package/lib/commonjs/plugins/insights/sender.js +0 -128
  122. package/lib/commonjs/plugins/insights/sender.js.map +0 -1
  123. package/lib/commonjs/plugins/insights/sender.spec.js +0 -265
  124. package/lib/commonjs/plugins/insights/sender.spec.js.map +0 -1
  125. package/lib/commonjs/plugins/insights/state.js +0 -145
  126. package/lib/commonjs/plugins/insights/state.js.map +0 -1
  127. package/lib/commonjs/releaseCatalogCache.spec.js +0 -213
  128. package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
  129. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
  130. package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  131. package/lib/commonjs/store.notification.spec.js +0 -84
  132. package/lib/commonjs/store.notification.spec.js.map +0 -1
  133. package/lib/commonjs/store.spec.js +0 -182
  134. package/lib/commonjs/store.spec.js.map +0 -1
  135. package/lib/commonjs/updateError.spec.js +0 -149
  136. package/lib/commonjs/updateError.spec.js.map +0 -1
  137. package/lib/commonjs/wrap.progress.spec.js +0 -195
  138. package/lib/commonjs/wrap.progress.spec.js.map +0 -1
  139. package/lib/commonjs/wrap.spec.js +0 -232
  140. package/lib/commonjs/wrap.spec.js.map +0 -1
  141. package/lib/module/appReady.spec.js +0 -359
  142. package/lib/module/appReady.spec.js.map +0 -1
  143. package/lib/module/appReady.test-utils.js +0 -19
  144. package/lib/module/appReady.test-utils.js.map +0 -1
  145. package/lib/module/baseURL.js +0 -11
  146. package/lib/module/baseURL.js.map +0 -1
  147. package/lib/module/catalogCacheNative.spec.js +0 -31
  148. package/lib/module/catalogCacheNative.spec.js.map +0 -1
  149. package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -935
  150. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
  151. package/lib/module/entryImportGraph.spec.js +0 -50
  152. package/lib/module/entryImportGraph.spec.js.map +0 -1
  153. package/lib/module/httpClient.spec.js +0 -171
  154. package/lib/module/httpClient.spec.js.map +0 -1
  155. package/lib/module/index.spec.js +0 -394
  156. package/lib/module/index.spec.js.map +0 -1
  157. package/lib/module/iosInterop.spec.js +0 -18
  158. package/lib/module/iosInterop.spec.js.map +0 -1
  159. package/lib/module/native.spec.js +0 -909
  160. package/lib/module/native.spec.js.map +0 -1
  161. package/lib/module/pluginHost.spec.js +0 -291
  162. package/lib/module/pluginHost.spec.js.map +0 -1
  163. package/lib/module/plugins/insights/eventId.js +0 -122
  164. package/lib/module/plugins/insights/eventId.js.map +0 -1
  165. package/lib/module/plugins/insights/eventId.spec.js +0 -47
  166. package/lib/module/plugins/insights/eventId.spec.js.map +0 -1
  167. package/lib/module/plugins/insights/index.js +0 -268
  168. package/lib/module/plugins/insights/index.js.map +0 -1
  169. package/lib/module/plugins/insights/insights.spec.js +0 -639
  170. package/lib/module/plugins/insights/insights.spec.js.map +0 -1
  171. package/lib/module/plugins/insights/sender.js +0 -123
  172. package/lib/module/plugins/insights/sender.js.map +0 -1
  173. package/lib/module/plugins/insights/sender.spec.js +0 -265
  174. package/lib/module/plugins/insights/sender.spec.js.map +0 -1
  175. package/lib/module/plugins/insights/state.js +0 -139
  176. package/lib/module/plugins/insights/state.js.map +0 -1
  177. package/lib/module/releaseCatalogCache.spec.js +0 -213
  178. package/lib/module/releaseCatalogCache.spec.js.map +0 -1
  179. package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
  180. package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
  181. package/lib/module/store.notification.spec.js +0 -85
  182. package/lib/module/store.notification.spec.js.map +0 -1
  183. package/lib/module/store.spec.js +0 -181
  184. package/lib/module/store.spec.js.map +0 -1
  185. package/lib/module/updateError.spec.js +0 -149
  186. package/lib/module/updateError.spec.js.map +0 -1
  187. package/lib/module/wrap.progress.spec.js +0 -196
  188. package/lib/module/wrap.progress.spec.js.map +0 -1
  189. package/lib/module/wrap.spec.js +0 -233
  190. package/lib/module/wrap.spec.js.map +0 -1
  191. package/lib/typescript/commonjs/appReady.test-utils.d.ts +0 -9
  192. package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +0 -1
  193. package/lib/typescript/commonjs/baseURL.d.ts +0 -4
  194. package/lib/typescript/commonjs/baseURL.d.ts.map +0 -1
  195. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +0 -18
  196. package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +0 -1
  197. package/lib/typescript/commonjs/plugins/insights/index.d.ts +0 -40
  198. package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +0 -1
  199. package/lib/typescript/commonjs/plugins/insights/sender.d.ts +0 -74
  200. package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +0 -1
  201. package/lib/typescript/commonjs/plugins/insights/state.d.ts +0 -46
  202. package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +0 -1
  203. package/lib/typescript/module/appReady.test-utils.d.ts +0 -9
  204. package/lib/typescript/module/appReady.test-utils.d.ts.map +0 -1
  205. package/lib/typescript/module/baseURL.d.ts +0 -4
  206. package/lib/typescript/module/baseURL.d.ts.map +0 -1
  207. package/lib/typescript/module/plugins/insights/eventId.d.ts +0 -18
  208. package/lib/typescript/module/plugins/insights/eventId.d.ts.map +0 -1
  209. package/lib/typescript/module/plugins/insights/index.d.ts +0 -40
  210. package/lib/typescript/module/plugins/insights/index.d.ts.map +0 -1
  211. package/lib/typescript/module/plugins/insights/sender.d.ts +0 -74
  212. package/lib/typescript/module/plugins/insights/sender.d.ts.map +0 -1
  213. package/lib/typescript/module/plugins/insights/state.d.ts +0 -46
  214. package/lib/typescript/module/plugins/insights/state.d.ts.map +0 -1
  215. package/plugins/insights/package.json +0 -8
  216. package/src/appReady.spec.ts +0 -393
  217. package/src/appReady.test-utils.ts +0 -31
  218. package/src/baseURL.ts +0 -15
  219. package/src/catalogCacheNative.spec.ts +0 -63
  220. package/src/checkForUpdate.releaseCatalog.spec.ts +0 -966
  221. package/src/entryImportGraph.spec.ts +0 -65
  222. package/src/httpClient.spec.ts +0 -216
  223. package/src/index.spec.ts +0 -434
  224. package/src/iosInterop.spec.ts +0 -30
  225. package/src/native.spec.ts +0 -978
  226. package/src/pluginHost.spec.ts +0 -297
  227. package/src/plugins/insights/eventId.spec.ts +0 -60
  228. package/src/plugins/insights/eventId.ts +0 -171
  229. package/src/plugins/insights/index.ts +0 -367
  230. package/src/plugins/insights/insights.spec.ts +0 -732
  231. package/src/plugins/insights/sender.spec.ts +0 -287
  232. package/src/plugins/insights/sender.ts +0 -221
  233. package/src/plugins/insights/state.ts +0 -184
  234. package/src/releaseCatalogCache.spec.ts +0 -304
  235. package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
  236. package/src/store.notification.spec.ts +0 -102
  237. package/src/store.spec.ts +0 -215
  238. package/src/updateError.spec.ts +0 -144
  239. package/src/wrap.progress.spec.ts +0 -232
  240. package/src/wrap.spec.ts +0 -262
@@ -1,297 +0,0 @@
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
- });
@@ -1,60 +0,0 @@
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
- });
@@ -1,171 +0,0 @@
1
- type RandomValuesSource = {
2
- readonly getRandomValues?: (array: Uint8Array) => Uint8Array;
3
- };
4
-
5
- const formatUUIDv7 = (bytes: Uint8Array, timestamp: number): string => {
6
- // Division, not bit shifts: shifts truncate to 32 bits, and a millisecond
7
- // timestamp already needs 41.
8
- let time = timestamp;
9
- for (let index = 5; index >= 0; index -= 1) {
10
- bytes[index] = time % 256;
11
- time = Math.floor(time / 256);
12
- }
13
- bytes[6] = 0x70 | (bytes[6]! & 0x0f);
14
- bytes[8] = 0x80 | (bytes[8]! & 0x3f);
15
-
16
- const hex = Array.from(bytes, (byte) =>
17
- byte.toString(16).padStart(2, "0"),
18
- ).join("");
19
- return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
20
- };
21
-
22
- /**
23
- * Creates an RFC 9562 UUIDv7: a 48-bit millisecond timestamp, then random bits.
24
- *
25
- * `createUUIDv7` from `@hot-updater/plugin-core` calls `crypto.getRandomValues`
26
- * unconditionally, but React Native has no Web Crypto unless the app installs a
27
- * polyfill. An Insights event ID only has to tell one event from another so a
28
- * retried POST is counted once, so `Math.random` is enough without one.
29
- */
30
- export const createUUIDv7 = (): string => {
31
- const bytes = new Uint8Array(16);
32
- const { crypto } = globalThis as { crypto?: RandomValuesSource };
33
- if (typeof crypto?.getRandomValues === "function") {
34
- crypto.getRandomValues(bytes);
35
- } else {
36
- for (let index = 0; index < bytes.length; index += 1) {
37
- bytes[index] = Math.floor(Math.random() * 256);
38
- }
39
- }
40
- return formatUUIDv7(bytes, Date.now());
41
- };
42
-
43
- const SHA256_K = [
44
- 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
45
- 0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
46
- 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
47
- 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
48
- 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
49
- 0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
50
- 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
51
- 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
52
- 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
53
- 0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
54
- 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
55
- ];
56
-
57
- const encodeUtf8 = (value: string): number[] => {
58
- const bytes: number[] = [];
59
- for (const character of value) {
60
- const code = character.codePointAt(0)!;
61
- if (code < 0x80) {
62
- bytes.push(code);
63
- } else if (code < 0x800) {
64
- bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
65
- } else if (code < 0x10000) {
66
- bytes.push(
67
- 0xe0 | (code >> 12),
68
- 0x80 | ((code >> 6) & 0x3f),
69
- 0x80 | (code & 0x3f),
70
- );
71
- } else {
72
- bytes.push(
73
- 0xf0 | (code >> 18),
74
- 0x80 | ((code >> 12) & 0x3f),
75
- 0x80 | ((code >> 6) & 0x3f),
76
- 0x80 | (code & 0x3f),
77
- );
78
- }
79
- }
80
- return bytes;
81
- };
82
-
83
- /** SHA-256 of a string's UTF-8 bytes, since React Native has no Web Crypto. */
84
- export const sha256 = (value: string): Uint8Array => {
85
- const bytes = encodeUtf8(value);
86
- const bitLength = bytes.length * 8;
87
- bytes.push(0x80);
88
- while (bytes.length % 64 !== 56) bytes.push(0);
89
- for (let shift = 56; shift >= 0; shift -= 8) {
90
- // The length's high word, as division, since shifts truncate to 32 bits.
91
- bytes.push(
92
- shift >= 32
93
- ? Math.floor(bitLength / 2 ** shift) & 0xff
94
- : (bitLength >>> shift) & 0xff,
95
- );
96
- }
97
-
98
- const hash = [
99
- 0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
100
- 0x1f83d9ab, 0x5be0cd19,
101
- ];
102
- const words: number[] = Array.from({ length: 64 }, () => 0);
103
- const rotate = (word: number, bits: number) =>
104
- (word >>> bits) | (word << (32 - bits));
105
-
106
- for (let offset = 0; offset < bytes.length; offset += 64) {
107
- for (let index = 0; index < 16; index += 1) {
108
- const at = offset + index * 4;
109
- words[index] =
110
- (bytes[at]! << 24) |
111
- (bytes[at + 1]! << 16) |
112
- (bytes[at + 2]! << 8) |
113
- bytes[at + 3]!;
114
- }
115
- for (let index = 16; index < 64; index += 1) {
116
- const low = words[index - 15]!;
117
- const high = words[index - 2]!;
118
- const s0 = rotate(low, 7) ^ rotate(low, 18) ^ (low >>> 3);
119
- const s1 = rotate(high, 17) ^ rotate(high, 19) ^ (high >>> 10);
120
- words[index] = (words[index - 16]! + s0 + words[index - 7]! + s1) | 0;
121
- }
122
-
123
- let [a, b, c, d, e, f, g, h] = hash as [
124
- number,
125
- number,
126
- number,
127
- number,
128
- number,
129
- number,
130
- number,
131
- number,
132
- ];
133
- for (let index = 0; index < 64; index += 1) {
134
- const s1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25);
135
- const choose = (e & f) ^ (~e & g);
136
- const temp1 = (h + s1 + choose + SHA256_K[index]! + words[index]!) | 0;
137
- const s0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22);
138
- const majority = (a & b) ^ (a & c) ^ (b & c);
139
- const temp2 = (s0 + majority) | 0;
140
- h = g;
141
- g = f;
142
- f = e;
143
- e = (d + temp1) | 0;
144
- d = c;
145
- c = b;
146
- b = a;
147
- a = (temp1 + temp2) | 0;
148
- }
149
- const results = [a, b, c, d, e, f, g, h];
150
- for (let index = 0; index < 8; index += 1) {
151
- hash[index] = (hash[index]! + results[index]!) | 0;
152
- }
153
- }
154
-
155
- const digest = new Uint8Array(32);
156
- hash.forEach((word, index) => {
157
- digest[index * 4] = (word >>> 24) & 0xff;
158
- digest[index * 4 + 1] = (word >>> 16) & 0xff;
159
- digest[index * 4 + 2] = (word >>> 8) & 0xff;
160
- digest[index * 4 + 3] = word & 0xff;
161
- });
162
- return digest;
163
- };
164
-
165
- /**
166
- * A UUIDv7 derived from a key: its time field is `timestamp` and its random
167
- * bits are the key's SHA-256, so the same key and time always give the same
168
- * id, and a server that stores reports by id counts it once.
169
- */
170
- export const createKeyedUUIDv7 = (key: string, timestamp: number): string =>
171
- formatUUIDv7(sha256(key).slice(0, 16), timestamp);