@hot-updater/react-native 1.0.0-rc.0 → 1.0.0-rc.14

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 (143) hide show
  1. package/android/proguard-rules.pro +15 -0
  2. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +1 -1
  3. package/android/src/main/java/com/hotupdater/BundleMetadata.kt +7 -0
  4. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +37 -54
  5. package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryState.kt +64 -0
  6. package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +97 -0
  7. package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +44 -0
  8. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +102 -20
  9. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.h +16 -0
  10. package/ios/HotUpdater/Recovery/HotUpdaterRecovery.mm +83 -0
  11. package/ios/HotUpdater/Recovery/HotUpdaterRecoveryHooks.mm +49 -0
  12. package/lib/commonjs/checkForUpdate.js +26 -14
  13. package/lib/commonjs/checkForUpdate.js.map +1 -1
  14. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +89 -25
  15. package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  16. package/lib/commonjs/httpClient.js +2 -2
  17. package/lib/commonjs/httpClient.js.map +1 -1
  18. package/lib/commonjs/index.js +32 -11
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.spec.js +59 -14
  21. package/lib/commonjs/index.spec.js.map +1 -1
  22. package/lib/commonjs/native.js +15 -11
  23. package/lib/commonjs/native.js.map +1 -1
  24. package/lib/commonjs/native.spec.js +107 -6
  25. package/lib/commonjs/native.spec.js.map +1 -1
  26. package/lib/commonjs/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +21 -21
  27. package/lib/commonjs/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  28. package/lib/commonjs/notifyAppReadyInsights.js +185 -0
  29. package/lib/commonjs/notifyAppReadyInsights.js.map +1 -0
  30. package/lib/commonjs/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +102 -29
  31. package/lib/commonjs/notifyAppReadyInsights.spec.js.map +1 -0
  32. package/lib/commonjs/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  33. package/lib/commonjs/notifyAppReadyInsights.test-utils.js.map +1 -0
  34. package/lib/commonjs/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +7 -7
  35. package/lib/commonjs/notifyAppReadyInsights.transport.spec.js.map +1 -0
  36. package/lib/commonjs/sdkVersion.js +1 -1
  37. package/lib/commonjs/sdkVersion.js.map +1 -1
  38. package/lib/commonjs/wrap.js +10 -9
  39. package/lib/commonjs/wrap.js.map +1 -1
  40. package/lib/commonjs/wrap.spec.js +90 -25
  41. package/lib/commonjs/wrap.spec.js.map +1 -1
  42. package/lib/module/checkForUpdate.js +27 -15
  43. package/lib/module/checkForUpdate.js.map +1 -1
  44. package/lib/module/checkForUpdate.releaseCatalog.spec.js +89 -25
  45. package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +1 -1
  46. package/lib/module/httpClient.js +2 -2
  47. package/lib/module/httpClient.js.map +1 -1
  48. package/lib/module/index.js +33 -12
  49. package/lib/module/index.js.map +1 -1
  50. package/lib/module/index.spec.js +59 -14
  51. package/lib/module/index.spec.js.map +1 -1
  52. package/lib/module/native.js +12 -8
  53. package/lib/module/native.js.map +1 -1
  54. package/lib/module/native.spec.js +107 -6
  55. package/lib/module/native.spec.js.map +1 -1
  56. package/lib/module/{notifyAppReadyAnalytics.boundary.spec.js → notifyAppReadyInsights.boundary.spec.js} +16 -16
  57. package/lib/module/notifyAppReadyInsights.boundary.spec.js.map +1 -0
  58. package/lib/module/notifyAppReadyInsights.js +178 -0
  59. package/lib/module/notifyAppReadyInsights.js.map +1 -0
  60. package/lib/module/{notifyAppReadyAnalytics.spec.js → notifyAppReadyInsights.spec.js} +94 -21
  61. package/lib/module/notifyAppReadyInsights.spec.js.map +1 -0
  62. package/lib/module/{notifyAppReadyAnalytics.test-utils.js → notifyAppReadyInsights.test-utils.js} +3 -3
  63. package/lib/module/notifyAppReadyInsights.test-utils.js.map +1 -0
  64. package/lib/module/{notifyAppReadyAnalytics.transport.spec.js → notifyAppReadyInsights.transport.spec.js} +4 -4
  65. package/lib/module/notifyAppReadyInsights.transport.spec.js.map +1 -0
  66. package/lib/module/sdkVersion.js +1 -1
  67. package/lib/module/sdkVersion.js.map +1 -1
  68. package/lib/module/wrap.js +10 -9
  69. package/lib/module/wrap.js.map +1 -1
  70. package/lib/module/wrap.spec.js +92 -25
  71. package/lib/module/wrap.spec.js.map +1 -1
  72. package/lib/typescript/commonjs/checkForUpdate.d.ts +1 -1
  73. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  74. package/lib/typescript/commonjs/httpClient.d.ts +6 -6
  75. package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/index.d.ts +6 -7
  77. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  78. package/lib/typescript/commonjs/native.d.ts +9 -6
  79. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  80. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts +25 -0
  81. package/lib/typescript/commonjs/notifyAppReadyInsights.d.ts.map +1 -0
  82. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts +8 -0
  83. package/lib/typescript/commonjs/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  84. package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
  85. package/lib/typescript/commonjs/sdkVersion.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
  87. package/lib/typescript/commonjs/wrap.types.d.ts +5 -6
  88. package/lib/typescript/commonjs/wrap.types.d.ts.map +1 -1
  89. package/lib/typescript/module/checkForUpdate.d.ts +1 -1
  90. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  91. package/lib/typescript/module/httpClient.d.ts +6 -6
  92. package/lib/typescript/module/httpClient.d.ts.map +1 -1
  93. package/lib/typescript/module/index.d.ts +6 -7
  94. package/lib/typescript/module/index.d.ts.map +1 -1
  95. package/lib/typescript/module/native.d.ts +9 -6
  96. package/lib/typescript/module/native.d.ts.map +1 -1
  97. package/lib/typescript/module/notifyAppReadyInsights.d.ts +25 -0
  98. package/lib/typescript/module/notifyAppReadyInsights.d.ts.map +1 -0
  99. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts +8 -0
  100. package/lib/typescript/module/notifyAppReadyInsights.test-utils.d.ts.map +1 -0
  101. package/lib/typescript/module/sdkVersion.d.ts +1 -1
  102. package/lib/typescript/module/sdkVersion.d.ts.map +1 -1
  103. package/lib/typescript/module/wrap.d.ts.map +1 -1
  104. package/lib/typescript/module/wrap.types.d.ts +5 -6
  105. package/lib/typescript/module/wrap.types.d.ts.map +1 -1
  106. package/package.json +3 -3
  107. package/src/checkForUpdate.releaseCatalog.spec.ts +128 -58
  108. package/src/checkForUpdate.ts +32 -20
  109. package/src/httpClient.ts +11 -11
  110. package/src/index.spec.ts +62 -14
  111. package/src/index.ts +44 -16
  112. package/src/native.spec.ts +110 -6
  113. package/src/native.ts +15 -12
  114. package/src/{notifyAppReadyAnalytics.boundary.spec.ts → notifyAppReadyInsights.boundary.spec.ts} +17 -17
  115. package/src/{notifyAppReadyAnalytics.spec.ts → notifyAppReadyInsights.spec.ts} +81 -22
  116. package/src/{notifyAppReadyAnalytics.test-utils.ts → notifyAppReadyInsights.test-utils.ts} +4 -4
  117. package/src/{notifyAppReadyAnalytics.transport.spec.ts → notifyAppReadyInsights.transport.spec.ts} +5 -5
  118. package/src/notifyAppReadyInsights.ts +281 -0
  119. package/src/sdkVersion.ts +1 -1
  120. package/src/wrap.spec.ts +101 -31
  121. package/src/wrap.tsx +11 -9
  122. package/src/wrap.types.ts +5 -6
  123. package/lib/commonjs/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  124. package/lib/commonjs/notifyAppReadyAnalytics.js +0 -133
  125. package/lib/commonjs/notifyAppReadyAnalytics.js.map +0 -1
  126. package/lib/commonjs/notifyAppReadyAnalytics.spec.js.map +0 -1
  127. package/lib/commonjs/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  128. package/lib/commonjs/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  129. package/lib/module/notifyAppReadyAnalytics.boundary.spec.js.map +0 -1
  130. package/lib/module/notifyAppReadyAnalytics.js +0 -128
  131. package/lib/module/notifyAppReadyAnalytics.js.map +0 -1
  132. package/lib/module/notifyAppReadyAnalytics.spec.js.map +0 -1
  133. package/lib/module/notifyAppReadyAnalytics.test-utils.js.map +0 -1
  134. package/lib/module/notifyAppReadyAnalytics.transport.spec.js.map +0 -1
  135. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts +0 -13
  136. package/lib/typescript/commonjs/notifyAppReadyAnalytics.d.ts.map +0 -1
  137. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  138. package/lib/typescript/commonjs/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  139. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts +0 -13
  140. package/lib/typescript/module/notifyAppReadyAnalytics.d.ts.map +0 -1
  141. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts +0 -8
  142. package/lib/typescript/module/notifyAppReadyAnalytics.test-utils.d.ts.map +0 -1
  143. package/src/notifyAppReadyAnalytics.ts +0 -194
@@ -1,13 +1,13 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type {
4
- NotifyAppReadyAnalyticsEvent,
4
+ NotifyAppReadyInsightsEvent,
5
5
  NotifyAppReadyResult,
6
6
  } from "./native";
7
7
  import {
8
8
  createNotifyReadResult,
9
9
  stubNotifyFrame,
10
- } from "./notifyAppReadyAnalytics.test-utils";
10
+ } from "./notifyAppReadyInsights.test-utils";
11
11
 
12
12
  vi.mock("react-native", () => ({
13
13
  Platform: { OS: "ios" },
@@ -25,7 +25,7 @@ const mocks = vi.hoisted(() => ({
25
25
  getPersistedUserIdentity: vi.fn(() => ({})),
26
26
  readNotifyAppReady: vi.fn<
27
27
  () => {
28
- analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
28
+ insightsEvent: NotifyAppReadyInsightsEvent | null;
29
29
  pending: boolean;
30
30
  result: NotifyAppReadyResult;
31
31
  }
@@ -51,20 +51,20 @@ vi.mock("./native", () => ({
51
51
  }));
52
52
 
53
53
  const createClient = () => {
54
- const sendAnalyticsEvent = vi.fn().mockResolvedValue(undefined);
54
+ const sendInsightsEvent = vi.fn().mockResolvedValue(undefined);
55
55
  return {
56
56
  client: {
57
57
  createSession: vi.fn(async () => ({
58
58
  fetchReleaseCatalog: vi.fn(),
59
59
  resolveArtifact: vi.fn(),
60
- sendAnalyticsEvent,
60
+ sendInsightsEvent,
61
61
  })),
62
62
  },
63
- sendAnalyticsEvent,
63
+ sendInsightsEvent,
64
64
  };
65
65
  };
66
66
 
67
- describe("automatic notifyAppReady analytics", () => {
67
+ describe("automatic notifyAppReady insights", () => {
68
68
  beforeEach(() => {
69
69
  vi.resetModules();
70
70
  vi.unstubAllGlobals();
@@ -85,6 +85,65 @@ describe("automatic notifyAppReady analytics", () => {
85
85
  mocks.readNotifyAppReady.mockReturnValue(createNotifyReadResult());
86
86
  });
87
87
 
88
+ it("orders download after in-flight startup reporting and suppresses a later no-change report", async () => {
89
+ const { handleNotifyAppReady, reportBundleDownloaded, reportNoChange } =
90
+ await import("./notifyAppReadyInsights");
91
+ const { client, sendInsightsEvent } = createClient();
92
+ let finishStartup!: () => void;
93
+ sendInsightsEvent.mockImplementationOnce(
94
+ () =>
95
+ new Promise<void>((resolve) => {
96
+ finishStartup = resolve;
97
+ }),
98
+ );
99
+ const options = { client, insights: true };
100
+ const startup = handleNotifyAppReady(options);
101
+ await vi.waitFor(() => expect(sendInsightsEvent).toHaveBeenCalledOnce());
102
+ const download = reportBundleDownloaded(options, {
103
+ fromBundleId: "bundle-id",
104
+ fromReleaseId: null,
105
+ toBundleId: "next-bundle",
106
+ toReleaseId: "next-release",
107
+ channel: "production",
108
+ updateStrategy: "appVersion",
109
+ });
110
+ await Promise.resolve();
111
+ expect(sendInsightsEvent).toHaveBeenCalledOnce();
112
+ finishStartup();
113
+ await Promise.all([startup, download]);
114
+ await reportNoChange(options);
115
+ expect(sendInsightsEvent.mock.calls.map(([params]) => params.type)).toEqual(
116
+ ["UNCHANGED", "UPDATE_DOWNLOADED"],
117
+ );
118
+ });
119
+
120
+ it("reports selecting a previous download again after a different pending bundle", async () => {
121
+ const { reportBundleDownloaded } = await import("./notifyAppReadyInsights");
122
+ const { client, sendInsightsEvent } = createClient();
123
+ const transition = {
124
+ fromBundleId: "bundle-id",
125
+ fromReleaseId: null,
126
+ toBundleId: "next-bundle",
127
+ toReleaseId: "next-release",
128
+ channel: "production",
129
+ updateStrategy: "appVersion" as const,
130
+ };
131
+ await reportBundleDownloaded({ client, insights: true }, transition);
132
+ await reportBundleDownloaded({ client, insights: true }, transition);
133
+ await reportBundleDownloaded(
134
+ { client, insights: true },
135
+ {
136
+ ...transition,
137
+ toBundleId: "other-bundle",
138
+ toReleaseId: "other-release",
139
+ },
140
+ );
141
+ await reportBundleDownloaded({ client, insights: true }, transition);
142
+ expect(
143
+ sendInsightsEvent.mock.calls.map(([params]) => params.toBundleId),
144
+ ).toEqual(["next-bundle", "other-bundle", "next-bundle"]);
145
+ });
146
+
88
147
  it("sends UPDATE_APPLIED with transition metadata and invokes readiness", async () => {
89
148
  stubNotifyFrame();
90
149
  mocks.getPersistedUserIdentity.mockReturnValue({
@@ -107,12 +166,12 @@ describe("automatic notifyAppReady analytics", () => {
107
166
  ),
108
167
  );
109
168
 
110
- const { client, sendAnalyticsEvent } = createClient();
169
+ const { client, sendInsightsEvent } = createClient();
111
170
  const onNotifyAppReady = vi.fn();
112
171
  const { init } = await import("./wrap");
113
172
 
114
173
  init({
115
- analytics: true,
174
+ insights: true,
116
175
  onNotifyAppReady,
117
176
  requestHeaders: { Authorization: "Bearer token" },
118
177
  requestTimeout: 1000,
@@ -120,7 +179,7 @@ describe("automatic notifyAppReady analytics", () => {
120
179
  });
121
180
  await vi.runOnlyPendingTimersAsync();
122
181
 
123
- expect(sendAnalyticsEvent).toHaveBeenCalledWith({
182
+ expect(sendInsightsEvent).toHaveBeenCalledWith({
124
183
  appVersion: "1.0.0",
125
184
  channel: "production",
126
185
  cohort: "123",
@@ -162,14 +221,14 @@ describe("automatic notifyAppReady analytics", () => {
162
221
  },
163
222
  ),
164
223
  );
165
- const { client, sendAnalyticsEvent } = createClient();
224
+ const { client, sendInsightsEvent } = createClient();
166
225
  const onNotifyAppReady = vi.fn();
167
226
  const { init } = await import("./wrap");
168
227
 
169
- init({ analytics: true, client, onNotifyAppReady });
228
+ init({ insights: true, client, onNotifyAppReady });
170
229
  await vi.runOnlyPendingTimersAsync();
171
230
 
172
- expect(sendAnalyticsEvent).toHaveBeenCalledWith({
231
+ expect(sendInsightsEvent).toHaveBeenCalledWith({
173
232
  appVersion: "1.0.0",
174
233
  channel: "production",
175
234
  cohort: "123",
@@ -194,16 +253,16 @@ describe("automatic notifyAppReady analytics", () => {
194
253
 
195
254
  it("sends one UNCHANGED event for repeated init calls", async () => {
196
255
  stubNotifyFrame();
197
- const { client, sendAnalyticsEvent } = createClient();
256
+ const { client, sendInsightsEvent } = createClient();
198
257
  const onNotifyAppReady = vi.fn();
199
258
  const { init } = await import("./wrap");
200
259
 
201
- init({ analytics: true, client, onNotifyAppReady });
202
- init({ analytics: true, client, onNotifyAppReady });
260
+ init({ insights: true, client, onNotifyAppReady });
261
+ init({ insights: true, client, onNotifyAppReady });
203
262
  await vi.runOnlyPendingTimersAsync();
204
263
 
205
- expect(sendAnalyticsEvent).toHaveBeenCalledTimes(1);
206
- expect(sendAnalyticsEvent).toHaveBeenCalledWith({
264
+ expect(sendInsightsEvent).toHaveBeenCalledTimes(1);
265
+ expect(sendInsightsEvent).toHaveBeenCalledWith({
207
266
  appVersion: "1.0.0",
208
267
  channel: "production",
209
268
  cohort: "123",
@@ -228,16 +287,16 @@ describe("automatic notifyAppReady analytics", () => {
228
287
  });
229
288
  });
230
289
 
231
- it("skips automatic analytics when disabled while preserving readiness", async () => {
290
+ it("skips automatic insights when disabled while preserving readiness", async () => {
232
291
  stubNotifyFrame();
233
- const { client, sendAnalyticsEvent } = createClient();
292
+ const { client, sendInsightsEvent } = createClient();
234
293
  const onNotifyAppReady = vi.fn();
235
294
  const { init } = await import("./wrap");
236
295
 
237
- init({ analytics: false, client, onNotifyAppReady });
296
+ init({ insights: false, client, onNotifyAppReady });
238
297
  await vi.runOnlyPendingTimersAsync();
239
298
 
240
- expect(sendAnalyticsEvent).not.toHaveBeenCalled();
299
+ expect(sendInsightsEvent).not.toHaveBeenCalled();
241
300
  expect(onNotifyAppReady).toHaveBeenCalledWith({ status: "UNCHANGED" });
242
301
  });
243
302
  });
@@ -1,20 +1,20 @@
1
1
  import { vi } from "vitest";
2
2
 
3
3
  import type {
4
- NotifyAppReadyAnalyticsEvent,
4
+ NotifyAppReadyInsightsEvent,
5
5
  NotifyAppReadyResult,
6
6
  } from "./native";
7
7
 
8
8
  export const createNotifyReadResult = (
9
9
  result: NotifyAppReadyResult = { status: "UNCHANGED" },
10
- analyticsEvent: NotifyAppReadyAnalyticsEvent | null = null,
10
+ insightsEvent: NotifyAppReadyInsightsEvent | null = null,
11
11
  pending = false,
12
12
  ): {
13
- analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
13
+ insightsEvent: NotifyAppReadyInsightsEvent | null;
14
14
  pending: boolean;
15
15
  result: NotifyAppReadyResult;
16
16
  } => ({
17
- analyticsEvent,
17
+ insightsEvent,
18
18
  pending,
19
19
  result,
20
20
  });
@@ -1,13 +1,13 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  import type {
4
- NotifyAppReadyAnalyticsEvent,
4
+ NotifyAppReadyInsightsEvent,
5
5
  NotifyAppReadyResult,
6
6
  } from "./native";
7
7
  import {
8
8
  createNotifyReadResult,
9
9
  stubNotifyFrame,
10
- } from "./notifyAppReadyAnalytics.test-utils";
10
+ } from "./notifyAppReadyInsights.test-utils";
11
11
 
12
12
  vi.mock("react-native", () => ({
13
13
  Platform: { OS: "ios" },
@@ -36,7 +36,7 @@ const mocks = vi.hoisted(() => ({
36
36
  getPersistedUserIdentity: vi.fn(() => ({})),
37
37
  readNotifyAppReady: vi.fn<
38
38
  () => {
39
- analyticsEvent: NotifyAppReadyAnalyticsEvent | null;
39
+ insightsEvent: NotifyAppReadyInsightsEvent | null;
40
40
  pending: boolean;
41
41
  result: NotifyAppReadyResult;
42
42
  }
@@ -110,7 +110,7 @@ describe("automatic notifyAppReady transport failures", () => {
110
110
  "https://updates.example.test/hot-updater",
111
111
  );
112
112
 
113
- init({ analytics: true, client, onError, onNotifyAppReady });
113
+ init({ insights: true, client, onError, onNotifyAppReady });
114
114
  await vi.runOnlyPendingTimersAsync();
115
115
 
116
116
  expect(fetchMock).toHaveBeenCalledTimes(1);
@@ -119,7 +119,7 @@ describe("automatic notifyAppReady transport failures", () => {
119
119
  status: "UNCHANGED",
120
120
  });
121
121
  expect(warn).toHaveBeenCalledWith(
122
- "[HotUpdater] Automatic notifyAppReady analytics failed:",
122
+ "[HotUpdater] Automatic notifyAppReady insights failed:",
123
123
  expect.any(Error),
124
124
  );
125
125
  } finally {
@@ -0,0 +1,281 @@
1
+ import { Platform } from "react-native";
2
+
3
+ import type { HotUpdaterError } from "./error";
4
+ import type { InsightsEventParams, HotUpdaterHttpClient } from "./httpClient";
5
+ import {
6
+ getAppVersion,
7
+ getBundleId,
8
+ getChannel,
9
+ getCohort,
10
+ getFingerprintHash,
11
+ getInstallId,
12
+ getPersistedUserIdentity,
13
+ type NotifyAppReadyInsightsEvent,
14
+ type NotifyAppReadyResult,
15
+ readNotifyAppReady,
16
+ } from "./native";
17
+
18
+ export type NotifyAppReadyOptions = {
19
+ insights?: boolean;
20
+ client: HotUpdaterHttpClient;
21
+ requestHeaders?: Record<string, string>;
22
+ requestTimeout?: number;
23
+ reportUnchanged?: boolean;
24
+ onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
25
+ onError?: (error: HotUpdaterError | Error | unknown) => void;
26
+ };
27
+
28
+ type RequestAnimationFrame = (callback: (timestamp: number) => void) => number;
29
+
30
+ let didAttemptAutomaticInsights = false;
31
+ let didDownload = false;
32
+ let readinessPromise: Promise<NotifyAppReadyResult | undefined> | undefined;
33
+
34
+ const waitForNextFrame = () =>
35
+ new Promise<void>((resolve) => {
36
+ const requestAnimationFrame = (
37
+ globalThis as typeof globalThis & {
38
+ requestAnimationFrame?: RequestAnimationFrame;
39
+ }
40
+ )?.requestAnimationFrame;
41
+
42
+ if (requestAnimationFrame) {
43
+ requestAnimationFrame(() => resolve());
44
+ return;
45
+ }
46
+
47
+ void Promise.resolve().then(resolve);
48
+ });
49
+
50
+ const assertNever = (value: never): never => {
51
+ throw new Error(`[HotUpdater] Unexpected notifyAppReady status: ${value}`);
52
+ };
53
+
54
+ const buildNotifyAppReadyInsightsParams = (
55
+ nativeResult: NotifyAppReadyResult,
56
+ insightsEvent: NotifyAppReadyInsightsEvent | null,
57
+ options: Pick<NotifyAppReadyOptions, "requestHeaders" | "requestTimeout">,
58
+ ): InsightsEventParams => {
59
+ const appVersion = getAppVersion();
60
+
61
+ if (!appVersion) {
62
+ throw new Error(
63
+ "[HotUpdater] Automatic insights requires a non-null native app version.",
64
+ );
65
+ }
66
+
67
+ const { userId, username } = getPersistedUserIdentity();
68
+ const installId = getInstallId();
69
+
70
+ if (!installId) {
71
+ throw new Error(
72
+ "[HotUpdater] Automatic insights requires a non-null native install id.",
73
+ );
74
+ }
75
+
76
+ const platform: "ios" | "android" =
77
+ Platform.OS === "android" ? "android" : "ios";
78
+
79
+ const commonParams = {
80
+ appVersion,
81
+ channel: getChannel(),
82
+ cohort: getCohort(),
83
+ fingerprintHash: getFingerprintHash(),
84
+ fromReleaseId: insightsEvent?.fromReleaseId ?? null,
85
+ installId,
86
+ platform,
87
+ requestHeaders: options.requestHeaders,
88
+ requestTimeout: options.requestTimeout,
89
+ toReleaseId: insightsEvent?.toReleaseId ?? null,
90
+ ...(userId != null ? { userId } : {}),
91
+ ...(username != null ? { username } : {}),
92
+ };
93
+
94
+ switch (nativeResult.status) {
95
+ case "UNCHANGED": {
96
+ const bundleId = getBundleId();
97
+
98
+ if (!bundleId) {
99
+ throw new Error(
100
+ "[HotUpdater] Automatic insights requires a non-null current bundle id.",
101
+ );
102
+ }
103
+
104
+ return {
105
+ ...commonParams,
106
+ fromBundleId: null,
107
+ toBundleId: bundleId,
108
+ type: "UNCHANGED",
109
+ updateStrategy: null,
110
+ };
111
+ }
112
+ case "UPDATE_APPLIED":
113
+ case "RECOVERED":
114
+ if (!insightsEvent) {
115
+ throw new Error(
116
+ "[HotUpdater] Native launch report is missing persisted metadata required for automatic insights.",
117
+ );
118
+ }
119
+
120
+ switch (insightsEvent.type) {
121
+ case "UPDATE_APPLIED":
122
+ return {
123
+ ...commonParams,
124
+ fromBundleId: insightsEvent.fromBundleId,
125
+ toBundleId: insightsEvent.toBundleId,
126
+ type: "UPDATE_APPLIED",
127
+ updateStrategy: insightsEvent.updateStrategy,
128
+ };
129
+ case "RECOVERED":
130
+ return {
131
+ ...commonParams,
132
+ fromBundleId: insightsEvent.fromBundleId,
133
+ toBundleId: insightsEvent.toBundleId,
134
+ type: "RECOVERED",
135
+ updateStrategy: insightsEvent.updateStrategy,
136
+ };
137
+ default:
138
+ return assertNever(insightsEvent.type);
139
+ }
140
+ default:
141
+ return assertNever(nativeResult);
142
+ }
143
+ };
144
+
145
+ let lastInsightsReport = Promise.resolve();
146
+ const sendInsights = (
147
+ options: Pick<NotifyAppReadyOptions, "client">,
148
+ params: InsightsEventParams,
149
+ ): Promise<void> => {
150
+ lastInsightsReport = lastInsightsReport
151
+ .catch(() => {})
152
+ .then(async () => {
153
+ const session = await options.client.createSession();
154
+ await session.sendInsightsEvent(params);
155
+ });
156
+ return lastInsightsReport;
157
+ };
158
+
159
+ const maybeSendAutomaticInsights = async (
160
+ options: NotifyAppReadyOptions,
161
+ nativeResult: NotifyAppReadyResult,
162
+ insightsEvent: NotifyAppReadyInsightsEvent | null,
163
+ ): Promise<void> => {
164
+ if (
165
+ !options.insights ||
166
+ didAttemptAutomaticInsights ||
167
+ (nativeResult.status === "UNCHANGED" &&
168
+ (options.reportUnchanged === false || didDownload))
169
+ ) {
170
+ return;
171
+ }
172
+
173
+ didAttemptAutomaticInsights = true;
174
+
175
+ if (nativeResult.status !== "UNCHANGED" && !insightsEvent) {
176
+ throw new Error(
177
+ "[HotUpdater] Native launch report is missing persisted metadata required for automatic insights.",
178
+ );
179
+ }
180
+
181
+ await sendInsights(
182
+ options,
183
+ buildNotifyAppReadyInsightsParams(nativeResult, insightsEvent, options),
184
+ );
185
+ };
186
+
187
+ const notifyAppReady = async (
188
+ options: NotifyAppReadyOptions,
189
+ ): Promise<NotifyAppReadyResult | undefined> => {
190
+ try {
191
+ let nativeReadResult: ReturnType<typeof readNotifyAppReady>;
192
+ do {
193
+ await waitForNextFrame();
194
+ nativeReadResult = readNotifyAppReady();
195
+ } while (nativeReadResult.pending);
196
+
197
+ const { insightsEvent, result: nativeResult } = nativeReadResult;
198
+
199
+ try {
200
+ await maybeSendAutomaticInsights(options, nativeResult, insightsEvent);
201
+ } catch (error) {
202
+ const warning = error instanceof Error ? error : new Error(String(error));
203
+ console.warn(
204
+ "[HotUpdater] Automatic notifyAppReady insights failed:",
205
+ warning,
206
+ );
207
+ }
208
+
209
+ options.onNotifyAppReady?.(nativeResult);
210
+ return nativeResult;
211
+ } catch (error) {
212
+ const normalizedError =
213
+ error instanceof Error ? error : new Error(String(error));
214
+ options.onError?.(error);
215
+ console.warn("[HotUpdater] Failed to notify app ready:", normalizedError);
216
+ return undefined;
217
+ }
218
+ };
219
+
220
+ export const handleNotifyAppReady = (
221
+ options: NotifyAppReadyOptions,
222
+ ): Promise<NotifyAppReadyResult | undefined> => {
223
+ readinessPromise = notifyAppReady(options);
224
+ return readinessPromise;
225
+ };
226
+
227
+ /** Called after an automatic update check finds no download or transition. */
228
+ export const reportNoChange = async (
229
+ options: NotifyAppReadyOptions,
230
+ ): Promise<void> => {
231
+ try {
232
+ await maybeSendAutomaticInsights(options, { status: "UNCHANGED" }, null);
233
+ } catch (error) {
234
+ console.warn("[HotUpdater] Automatic no-change insights failed:", error);
235
+ }
236
+ };
237
+
238
+ let lastDownloadedSelection: string | undefined;
239
+
240
+ /** Report only after the archive/diff has been verified and staged successfully. */
241
+ export const reportBundleDownloaded = async (
242
+ options: Pick<
243
+ NotifyAppReadyOptions,
244
+ "insights" | "client" | "requestHeaders" | "requestTimeout"
245
+ >,
246
+ transition: {
247
+ readonly fromBundleId: string;
248
+ readonly fromReleaseId: string | null;
249
+ readonly toBundleId: string;
250
+ readonly toReleaseId: string | null;
251
+ readonly channel: string;
252
+ readonly updateStrategy: "fingerprint" | "appVersion";
253
+ },
254
+ ): Promise<void> => {
255
+ if (!options.insights || transition.fromBundleId === transition.toBundleId)
256
+ return;
257
+ const key = JSON.stringify([
258
+ transition.channel,
259
+ transition.toReleaseId,
260
+ transition.toBundleId,
261
+ ]);
262
+ if (lastDownloadedSelection === key) return;
263
+ lastDownloadedSelection = key;
264
+ // A later init/mount in this JS runtime must not overwrite pending apply with UNCHANGED.
265
+ didDownload = true;
266
+ try {
267
+ await readinessPromise;
268
+ const common = buildNotifyAppReadyInsightsParams(
269
+ { status: "UNCHANGED" },
270
+ null,
271
+ options,
272
+ );
273
+ await sendInsights(options, {
274
+ ...common,
275
+ ...transition,
276
+ type: "UPDATE_DOWNLOADED",
277
+ });
278
+ } catch (error) {
279
+ console.warn("[HotUpdater] Download insights failed:", error);
280
+ }
281
+ };
package/src/sdkVersion.ts CHANGED
@@ -1 +1 @@
1
- export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.0";
1
+ export const HOT_UPDATER_SDK_VERSION = "1.0.0-rc.14";