@hot-updater/react-native 0.25.13 → 0.26.0

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 (45) hide show
  1. package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +42 -0
  2. package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +25 -0
  3. package/android/src/newarch/HotUpdater.kt +4 -1
  4. package/android/src/newarch/HotUpdaterModule.kt +18 -1
  5. package/android/src/oldarch/HotUpdater.kt +4 -1
  6. package/android/src/oldarch/HotUpdaterModule.kt +19 -1
  7. package/android/src/oldarch/HotUpdaterSpec.kt +2 -0
  8. package/ios/HotUpdater/Internal/BundleFileStorageService.swift +44 -0
  9. package/ios/HotUpdater/Internal/HotUpdater.mm +17 -0
  10. package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +44 -2
  11. package/lib/commonjs/checkForUpdate.js +29 -4
  12. package/lib/commonjs/checkForUpdate.js.map +1 -1
  13. package/lib/commonjs/index.js +35 -0
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/native.js +88 -21
  16. package/lib/commonjs/native.js.map +1 -1
  17. package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
  18. package/lib/module/checkForUpdate.js +29 -5
  19. package/lib/module/checkForUpdate.js.map +1 -1
  20. package/lib/module/index.js +36 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/native.js +81 -16
  23. package/lib/module/native.js.map +1 -1
  24. package/lib/module/specs/NativeHotUpdater.js.map +1 -1
  25. package/lib/typescript/commonjs/checkForUpdate.d.ts +5 -0
  26. package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/index.d.ts +26 -0
  28. package/lib/typescript/commonjs/index.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/native.d.ts +13 -0
  30. package/lib/typescript/commonjs/native.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +8 -0
  32. package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
  33. package/lib/typescript/module/checkForUpdate.d.ts +5 -0
  34. package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
  35. package/lib/typescript/module/index.d.ts +26 -0
  36. package/lib/typescript/module/index.d.ts.map +1 -1
  37. package/lib/typescript/module/native.d.ts +13 -0
  38. package/lib/typescript/module/native.d.ts.map +1 -1
  39. package/lib/typescript/module/specs/NativeHotUpdater.d.ts +8 -0
  40. package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
  41. package/package.json +6 -6
  42. package/src/checkForUpdate.ts +63 -3
  43. package/src/index.ts +41 -0
  44. package/src/native.ts +103 -16
  45. package/src/specs/NativeHotUpdater.ts +9 -0
package/src/native.ts CHANGED
@@ -15,6 +15,62 @@ export const HotUpdaterConstants = {
15
15
  HOT_UPDATER_BUNDLE_ID: __HOT_UPDATER_BUNDLE_ID || NIL_UUID,
16
16
  };
17
17
 
18
+ class HotUpdaterSessionState {
19
+ private readonly defaultChannel: string;
20
+ private currentChannel: string;
21
+ private readonly inflightUpdates = new Map<string, Promise<boolean>>();
22
+ private lastInstalledBundleId: string | null = null;
23
+
24
+ constructor() {
25
+ const constants = HotUpdaterNative.getConstants();
26
+ this.defaultChannel = constants.DEFAULT_CHANNEL ?? constants.CHANNEL;
27
+ this.currentChannel = constants.CHANNEL;
28
+ }
29
+
30
+ getChannel(): string {
31
+ return this.currentChannel;
32
+ }
33
+
34
+ getDefaultChannel(): string {
35
+ return this.defaultChannel;
36
+ }
37
+
38
+ isChannelSwitched(): boolean {
39
+ return this.currentChannel !== this.defaultChannel;
40
+ }
41
+
42
+ hasInstalledBundle(bundleId: string): boolean {
43
+ return this.lastInstalledBundleId === bundleId;
44
+ }
45
+
46
+ getInflightUpdate(bundleId: string): Promise<boolean> | undefined {
47
+ return this.inflightUpdates.get(bundleId);
48
+ }
49
+
50
+ trackInflightUpdate(bundleId: string, promise: Promise<boolean>) {
51
+ this.inflightUpdates.set(bundleId, promise);
52
+ }
53
+
54
+ clearInflightUpdate(bundleId: string) {
55
+ this.inflightUpdates.delete(bundleId);
56
+ }
57
+
58
+ markBundleInstalled(bundleId: string, channel?: string) {
59
+ this.lastInstalledBundleId = bundleId;
60
+ if (channel) {
61
+ this.currentChannel = channel;
62
+ }
63
+ }
64
+
65
+ resetChannelState() {
66
+ this.currentChannel = this.defaultChannel;
67
+ this.lastInstalledBundleId = null;
68
+ this.inflightUpdates.clear();
69
+ }
70
+ }
71
+
72
+ const sessionState = new HotUpdaterSessionState();
73
+
18
74
  export type HotUpdaterEvent = {
19
75
  onProgress: {
20
76
  progress: number;
@@ -36,13 +92,9 @@ export const addListener = <T extends keyof HotUpdaterEvent>(
36
92
 
37
93
  export type UpdateParams = UpdateBundleParams & {
38
94
  status: UpdateStatus;
95
+ shouldSkipCurrentBundleIdCheck?: boolean;
39
96
  };
40
97
 
41
- // In-flight update deduplication by bundleId (session-scoped).
42
- const inflightUpdates = new Map<string, Promise<boolean>>();
43
- // Tracks the last successfully installed bundleId for this session.
44
- let lastInstalledBundleId: string | null = null;
45
-
46
98
  /**
47
99
  * Downloads files and applies them to the app.
48
100
  *
@@ -71,24 +123,27 @@ export async function updateBundle(
71
123
  typeof paramsOrBundleId === "string" ? "UPDATE" : paramsOrBundleId.status;
72
124
 
73
125
  // If we have already installed this bundle in this session, skip re-download.
74
- if (status === "UPDATE" && lastInstalledBundleId === updateBundleId) {
126
+ if (status === "UPDATE" && sessionState.hasInstalledBundle(updateBundleId)) {
75
127
  return true;
76
128
  }
77
129
 
78
- const currentBundleId = getBundleId();
130
+ const shouldSkipCurrentBundleIdCheck =
131
+ typeof paramsOrBundleId === "string"
132
+ ? false
133
+ : paramsOrBundleId.shouldSkipCurrentBundleIdCheck === true;
79
134
 
80
- // updateBundleId <= currentBundleId
81
135
  if (
136
+ !shouldSkipCurrentBundleIdCheck &&
82
137
  status === "UPDATE" &&
83
- updateBundleId.localeCompare(currentBundleId) <= 0
138
+ updateBundleId.localeCompare(getBundleId()) <= 0
84
139
  ) {
85
140
  throw new Error(
86
- "Update bundle id is the same as the current bundle id. Preventing infinite update loop.",
141
+ "Update bundle id is not newer than the current bundle id. Preventing infinite update loop.",
87
142
  );
88
143
  }
89
144
 
90
145
  // In-flight guard: return the same promise if the same bundle is already updating.
91
- const existing = inflightUpdates.get(updateBundleId);
146
+ const existing = sessionState.getInflightUpdate(updateBundleId);
92
147
  if (existing) return existing;
93
148
 
94
149
  const targetFileUrl =
@@ -101,23 +156,27 @@ export async function updateBundle(
101
156
  ? undefined
102
157
  : paramsOrBundleId.fileHash;
103
158
 
159
+ const targetChannel =
160
+ typeof paramsOrBundleId === "string" ? undefined : paramsOrBundleId.channel;
161
+
104
162
  const promise = (async () => {
105
163
  try {
106
164
  const ok = await HotUpdaterNative.updateBundle({
107
165
  bundleId: updateBundleId,
166
+ channel: targetChannel,
108
167
  fileUrl: targetFileUrl,
109
168
  fileHash: targetFileHash ?? null,
110
169
  });
111
170
  if (ok) {
112
- lastInstalledBundleId = updateBundleId;
171
+ sessionState.markBundleInstalled(updateBundleId, targetChannel);
113
172
  }
114
173
  return ok;
115
174
  } finally {
116
- inflightUpdates.delete(updateBundleId);
175
+ sessionState.clearInflightUpdate(updateBundleId);
117
176
  }
118
177
  })();
119
178
 
120
- inflightUpdates.set(updateBundleId, promise);
179
+ sessionState.trackInflightUpdate(updateBundleId, promise);
121
180
  return promise;
122
181
  }
123
182
 
@@ -165,8 +224,21 @@ export const getBundleId = (): string => {
165
224
  * @returns {string} Resolves with the channel or null if not available.
166
225
  */
167
226
  export const getChannel = (): string => {
168
- const constants = HotUpdaterNative.getConstants();
169
- return constants.CHANNEL;
227
+ return sessionState.getChannel();
228
+ };
229
+
230
+ /**
231
+ * Fetches the build-time default channel for the app.
232
+ */
233
+ export const getDefaultChannel = (): string => {
234
+ return sessionState.getDefaultChannel();
235
+ };
236
+
237
+ /**
238
+ * Returns whether the app is currently using a runtime channel override.
239
+ */
240
+ export const isChannelSwitched = (): boolean => {
241
+ return sessionState.isChannelSwitched();
170
242
  };
171
243
 
172
244
  /**
@@ -275,3 +347,18 @@ export const getBaseURL = (): string | null => {
275
347
  }
276
348
  return null;
277
349
  };
350
+
351
+ /**
352
+ * Clears the runtime channel override and restores the original bundle.
353
+ */
354
+ export const resetChannel = async (): Promise<boolean> => {
355
+ if (!sessionState.isChannelSwitched()) {
356
+ return true;
357
+ }
358
+
359
+ const ok = await HotUpdaterNative.resetChannel();
360
+ if (ok) {
361
+ sessionState.resetChannelState();
362
+ }
363
+ return ok;
364
+ };
@@ -3,6 +3,7 @@ import { TurboModuleRegistry } from "react-native";
3
3
 
4
4
  export interface UpdateBundleParams {
5
5
  bundleId: string;
6
+ channel?: string;
6
7
  fileUrl: string | null;
7
8
  /**
8
9
  * File hash for integrity/signature verification.
@@ -84,6 +85,13 @@ export interface Spec extends TurboModule {
84
85
  */
85
86
  clearCrashHistory(): boolean;
86
87
 
88
+ /**
89
+ * Clears the runtime channel override and restores the original bundle.
90
+ *
91
+ * @returns Promise that resolves to true if successful
92
+ */
93
+ resetChannel(): Promise<boolean>;
94
+
87
95
  /**
88
96
  * Gets the base URL for the current active bundle directory.
89
97
  * Returns the file:// URL to the bundle directory without trailing slash.
@@ -100,6 +108,7 @@ export interface Spec extends TurboModule {
100
108
  MIN_BUNDLE_ID: string;
101
109
  APP_VERSION: string | null;
102
110
  CHANNEL: string;
111
+ DEFAULT_CHANNEL: string;
103
112
  FINGERPRINT_HASH: string | null;
104
113
  };
105
114
  }