@hot-updater/react-native 0.27.1 → 0.29.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.
- package/android/build.gradle +12 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/AndroidManifestNew.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +9 -0
- package/android/src/main/cpp/HotUpdaterRecovery.cpp +143 -0
- package/android/src/main/java/com/hotupdater/BundleFileStorageService.kt +325 -210
- package/android/src/main/java/com/hotupdater/BundleMetadata.kt +73 -16
- package/android/src/main/java/com/hotupdater/CohortService.kt +73 -0
- package/android/src/main/java/com/hotupdater/DecompressService.kt +28 -22
- package/android/src/main/java/com/hotupdater/HotUpdaterException.kt +1 -1
- package/android/src/main/java/com/hotupdater/HotUpdaterImpl.kt +51 -13
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryManager.kt +533 -0
- package/android/src/main/java/com/hotupdater/HotUpdaterRecoveryReceiver.kt +14 -0
- package/android/src/main/java/com/hotupdater/ReactNativeValueConverters.kt +55 -0
- package/android/src/main/java/com/hotupdater/TarBrDecompressionStrategy.kt +19 -7
- package/android/src/newarch/HotUpdaterModule.kt +16 -25
- package/android/src/oldarch/HotUpdaterModule.kt +20 -26
- package/android/src/oldarch/HotUpdaterSpec.kt +12 -2
- package/ios/HotUpdater/Internal/BundleFileStorageService.swift +340 -232
- package/ios/HotUpdater/Internal/BundleMetadata.swift +61 -8
- package/ios/HotUpdater/Internal/CohortService.swift +63 -0
- package/ios/HotUpdater/Internal/DecompressService.swift +53 -30
- package/ios/HotUpdater/Internal/HotUpdater-Bridging-Header.h +9 -1
- package/ios/HotUpdater/Internal/HotUpdater.mm +376 -70
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.h +7 -0
- package/ios/HotUpdater/Internal/HotUpdaterCrashHandler.mm +4 -0
- package/ios/HotUpdater/Internal/HotUpdaterImpl.swift +321 -9
- package/ios/HotUpdater/Internal/TarBrDecompressionStrategy.swift +24 -8
- package/lib/commonjs/DefaultResolver.js +3 -5
- package/lib/commonjs/DefaultResolver.js.map +1 -1
- package/lib/commonjs/checkForUpdate.js +2 -0
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/index.js +13 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +211 -39
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/native.spec.js +443 -0
- package/lib/commonjs/native.spec.js.map +1 -0
- package/lib/commonjs/specs/NativeHotUpdater.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/wrap.js +4 -5
- package/lib/commonjs/wrap.js.map +1 -1
- package/lib/module/DefaultResolver.js +3 -5
- package/lib/module/DefaultResolver.js.map +1 -1
- package/lib/module/checkForUpdate.js +3 -1
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/index.js +14 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +204 -34
- package/lib/module/native.js.map +1 -1
- package/lib/module/native.spec.js +442 -0
- package/lib/module/native.spec.js.map +1 -0
- package/lib/module/specs/NativeHotUpdater.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/wrap.js +5 -6
- package/lib/module/wrap.js.map +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +14 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +43 -23
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/commonjs/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +6 -3
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.d.ts +3 -6
- package/lib/typescript/commonjs/wrap.d.ts.map +1 -1
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +14 -1
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +43 -23
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts +32 -8
- package/lib/typescript/module/specs/NativeHotUpdater.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +6 -3
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.d.ts +3 -6
- package/lib/typescript/module/wrap.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/DefaultResolver.ts +4 -4
- package/src/checkForUpdate.ts +4 -0
- package/src/index.ts +21 -0
- package/src/native.spec.ts +480 -0
- package/src/native.ts +285 -39
- package/src/specs/NativeHotUpdater.ts +36 -6
- package/src/types.ts +7 -3
- package/src/wrap.tsx +8 -12
package/src/native.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import
|
|
1
|
+
import {
|
|
2
|
+
INVALID_COHORT_ERROR_MESSAGE,
|
|
3
|
+
isValidCohort,
|
|
4
|
+
normalizeCohortValue,
|
|
5
|
+
type UpdateStatus,
|
|
6
|
+
} from "@hot-updater/core";
|
|
2
7
|
import { NativeEventEmitter, Platform } from "react-native";
|
|
3
8
|
import { HotUpdaterErrorCode, isHotUpdaterError } from "./error";
|
|
4
9
|
import HotUpdaterNative, {
|
|
@@ -8,6 +13,30 @@ import HotUpdaterNative, {
|
|
|
8
13
|
export { HotUpdaterErrorCode, isHotUpdaterError };
|
|
9
14
|
|
|
10
15
|
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
|
|
16
|
+
const normalizeAndValidateCohort = (cohort: string): string => {
|
|
17
|
+
const normalized = normalizeCohortValue(cohort);
|
|
18
|
+
if (!isValidCohort(normalized)) {
|
|
19
|
+
throw new Error(INVALID_COHORT_ERROR_MESSAGE);
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface ManifestAsset {
|
|
25
|
+
fileHash: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Manifest {
|
|
29
|
+
bundleId: string;
|
|
30
|
+
assets: Record<string, ManifestAsset>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type ActiveBundleSnapshotCacheValues = {
|
|
34
|
+
bundleId?: string;
|
|
35
|
+
manifest?: Manifest;
|
|
36
|
+
baseURL?: string | null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type ActiveBundleSnapshotCacheKey = keyof ActiveBundleSnapshotCacheValues;
|
|
11
40
|
|
|
12
41
|
/**
|
|
13
42
|
* Built-in reload behaviors used by `HotUpdater.reload()`.
|
|
@@ -34,17 +63,16 @@ export type CustomReloadHandler = () => void | Promise<void>;
|
|
|
34
63
|
*/
|
|
35
64
|
export type ReloadBehaviorSetting = ReloadBehavior | "custom";
|
|
36
65
|
|
|
37
|
-
declare const __HOT_UPDATER_BUNDLE_ID: string | undefined;
|
|
38
|
-
|
|
39
|
-
export const HotUpdaterConstants = {
|
|
40
|
-
HOT_UPDATER_BUNDLE_ID: __HOT_UPDATER_BUNDLE_ID || NIL_UUID,
|
|
41
|
-
};
|
|
42
|
-
|
|
43
66
|
class HotUpdaterSessionState {
|
|
44
67
|
private readonly defaultChannel: string;
|
|
45
68
|
private currentChannel: string;
|
|
69
|
+
private cachedCohort: string | undefined;
|
|
46
70
|
private readonly inflightUpdates = new Map<string, Promise<boolean>>();
|
|
47
71
|
private lastInstalledBundleId: string | null = null;
|
|
72
|
+
private readonly activeBundleSnapshotCache = new Map<
|
|
73
|
+
ActiveBundleSnapshotCacheKey,
|
|
74
|
+
ActiveBundleSnapshotCacheValues[ActiveBundleSnapshotCacheKey]
|
|
75
|
+
>();
|
|
48
76
|
|
|
49
77
|
constructor() {
|
|
50
78
|
const constants = HotUpdaterNative.getConstants();
|
|
@@ -85,12 +113,66 @@ class HotUpdaterSessionState {
|
|
|
85
113
|
if (channel) {
|
|
86
114
|
this.currentChannel = channel;
|
|
87
115
|
}
|
|
116
|
+
this.clearActiveBundleSnapshotCache();
|
|
88
117
|
}
|
|
89
118
|
|
|
90
119
|
resetChannelState() {
|
|
91
120
|
this.currentChannel = this.defaultChannel;
|
|
92
121
|
this.lastInstalledBundleId = null;
|
|
93
122
|
this.inflightUpdates.clear();
|
|
123
|
+
this.clearActiveBundleSnapshotCache();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
getCachedBundleId(): string | undefined {
|
|
127
|
+
return this.getActiveBundleSnapshotValue("bundleId");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
cacheBundleId(bundleId: string) {
|
|
131
|
+
this.setActiveBundleSnapshotValue("bundleId", bundleId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
getCachedManifest(): Manifest | undefined {
|
|
135
|
+
const manifest = this.getActiveBundleSnapshotValue("manifest");
|
|
136
|
+
return manifest ? cloneManifest(manifest) : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
cacheManifest(manifest: Manifest) {
|
|
140
|
+
this.setActiveBundleSnapshotValue("manifest", cloneManifest(manifest));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
getCachedBaseURL(): string | null | undefined {
|
|
144
|
+
return this.getActiveBundleSnapshotValue("baseURL");
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
cacheBaseURL(baseURL: string | null) {
|
|
148
|
+
this.setActiveBundleSnapshotValue("baseURL", baseURL);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getCachedCohort(): string | undefined {
|
|
152
|
+
return this.cachedCohort;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
cacheCohort(cohort: string) {
|
|
156
|
+
this.cachedCohort = cohort;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private clearActiveBundleSnapshotCache() {
|
|
160
|
+
this.activeBundleSnapshotCache.clear();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private getActiveBundleSnapshotValue<K extends ActiveBundleSnapshotCacheKey>(
|
|
164
|
+
key: K,
|
|
165
|
+
): ActiveBundleSnapshotCacheValues[K] | undefined {
|
|
166
|
+
return this.activeBundleSnapshotCache.get(key) as
|
|
167
|
+
| ActiveBundleSnapshotCacheValues[K]
|
|
168
|
+
| undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
private setActiveBundleSnapshotValue<K extends ActiveBundleSnapshotCacheKey>(
|
|
172
|
+
key: K,
|
|
173
|
+
value: ActiveBundleSnapshotCacheValues[K],
|
|
174
|
+
) {
|
|
175
|
+
this.activeBundleSnapshotCache.set(key, value);
|
|
94
176
|
}
|
|
95
177
|
}
|
|
96
178
|
|
|
@@ -98,6 +180,16 @@ const sessionState = new HotUpdaterSessionState();
|
|
|
98
180
|
let reloadBehavior: ReloadBehaviorSetting = "processRestart";
|
|
99
181
|
let customReloadHandler: CustomReloadHandler | null = null;
|
|
100
182
|
|
|
183
|
+
const cloneManifest = (manifest: Manifest): Manifest => ({
|
|
184
|
+
bundleId: manifest.bundleId,
|
|
185
|
+
assets: Object.fromEntries(
|
|
186
|
+
Object.entries(manifest.assets).map(([key, asset]) => [
|
|
187
|
+
key,
|
|
188
|
+
{ fileHash: asset.fileHash },
|
|
189
|
+
]),
|
|
190
|
+
),
|
|
191
|
+
});
|
|
192
|
+
|
|
101
193
|
const getReloadProcess = (): (() => Promise<void>) | null => {
|
|
102
194
|
const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
|
|
103
195
|
reloadProcess?: () => Promise<void>;
|
|
@@ -318,13 +410,72 @@ export const getMinBundleId = (): string => {
|
|
|
318
410
|
/**
|
|
319
411
|
* Fetches the current bundle version id.
|
|
320
412
|
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
413
|
+
* JS falls back to MIN_BUNDLE_ID only after native confirms there is no active
|
|
414
|
+
* downloaded bundle. When the native module does not expose `getBundleId()`,
|
|
415
|
+
* treat it as a JS/native SDK mismatch instead of silently reporting the
|
|
416
|
+
* built-in bundle.
|
|
417
|
+
*
|
|
418
|
+
* @returns {string} Resolves with the current version id.
|
|
323
419
|
*/
|
|
324
420
|
export const getBundleId = (): string => {
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
421
|
+
const cachedBundleId = sessionState.getCachedBundleId();
|
|
422
|
+
if (cachedBundleId !== undefined) {
|
|
423
|
+
return cachedBundleId;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
|
|
427
|
+
getBundleId?: () => string | null;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
if (typeof nativeModule.getBundleId !== "function") {
|
|
431
|
+
throw new Error(
|
|
432
|
+
"[HotUpdater] Native module is missing 'getBundleId()'. This JS bundle requires a newer native @hot-updater/react-native SDK. Rebuild and release a new app version before delivering this OTA update.",
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const bundleId = nativeModule.getBundleId();
|
|
437
|
+
|
|
438
|
+
const resolvedBundleId =
|
|
439
|
+
!bundleId || bundleId === NIL_UUID ? getMinBundleId() : bundleId;
|
|
440
|
+
|
|
441
|
+
sessionState.cacheBundleId(resolvedBundleId);
|
|
442
|
+
return resolvedBundleId;
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Fetches the current manifest for the active bundle.
|
|
447
|
+
*
|
|
448
|
+
* Returns a normalized manifest with empty assets when manifest.json is missing
|
|
449
|
+
* or invalid.
|
|
450
|
+
*/
|
|
451
|
+
export const getManifest = (): Manifest => {
|
|
452
|
+
const cachedManifest = sessionState.getCachedManifest();
|
|
453
|
+
if (cachedManifest !== undefined) {
|
|
454
|
+
return cachedManifest;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const nativeModule = HotUpdaterNative as typeof HotUpdaterNative & {
|
|
458
|
+
getManifest?: () => Record<string, unknown> | string;
|
|
459
|
+
};
|
|
460
|
+
const manifest = nativeModule.getManifest?.();
|
|
461
|
+
|
|
462
|
+
let normalizedManifest: Manifest;
|
|
463
|
+
|
|
464
|
+
if (!manifest) {
|
|
465
|
+
normalizedManifest = createEmptyManifest();
|
|
466
|
+
} else if (typeof manifest === "string") {
|
|
467
|
+
try {
|
|
468
|
+
normalizedManifest = normalizeManifest(JSON.parse(manifest));
|
|
469
|
+
} catch {
|
|
470
|
+
normalizedManifest = createEmptyManifest();
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
normalizedManifest = normalizeManifest(manifest);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
sessionState.cacheBundleId(normalizedManifest.bundleId);
|
|
477
|
+
sessionState.cacheManifest(normalizedManifest);
|
|
478
|
+
return cloneManifest(normalizedManifest);
|
|
328
479
|
};
|
|
329
480
|
|
|
330
481
|
/**
|
|
@@ -364,18 +515,16 @@ export const getFingerprintHash = (): string | null => {
|
|
|
364
515
|
* Result returned by notifyAppReady()
|
|
365
516
|
*/
|
|
366
517
|
export type NotifyAppReadyResult = {
|
|
367
|
-
status: "
|
|
518
|
+
status: "RECOVERED" | "STABLE";
|
|
368
519
|
crashedBundleId?: string;
|
|
369
520
|
};
|
|
370
521
|
|
|
371
522
|
/**
|
|
372
|
-
*
|
|
373
|
-
* If the bundle matches the staging bundle, it promotes to stable.
|
|
523
|
+
* Reads the native launch report for the current process.
|
|
374
524
|
*
|
|
375
|
-
* This function is called automatically
|
|
525
|
+
* This function is called automatically after the app has rendered.
|
|
376
526
|
*
|
|
377
527
|
* @returns {NotifyAppReadyResult} Bundle state information
|
|
378
|
-
* - `status: "PROMOTED"` - Staging bundle was promoted to stable (ACTIVE event)
|
|
379
528
|
* - `status: "RECOVERED"` - App recovered from crash, rollback occurred (ROLLBACK event)
|
|
380
529
|
* - `status: "STABLE"` - No changes, already stable
|
|
381
530
|
* - `crashedBundleId` - Present only when status is "RECOVERED"
|
|
@@ -384,33 +533,97 @@ export type NotifyAppReadyResult = {
|
|
|
384
533
|
* ```ts
|
|
385
534
|
* const result = HotUpdater.notifyAppReady();
|
|
386
535
|
*
|
|
387
|
-
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* case "RECOVERED":
|
|
393
|
-
* // Send ROLLBACK analytics event
|
|
394
|
-
* analytics.track('bundle_rollback', { crashedBundleId: result.crashedBundleId });
|
|
395
|
-
* break;
|
|
396
|
-
* case "STABLE":
|
|
397
|
-
* // No special action needed
|
|
398
|
-
* break;
|
|
536
|
+
* if (result.status === "RECOVERED") {
|
|
537
|
+
* // Send ROLLBACK analytics event
|
|
538
|
+
* analytics.track("bundle_rollback", {
|
|
539
|
+
* crashedBundleId: result.crashedBundleId,
|
|
540
|
+
* });
|
|
399
541
|
* }
|
|
400
542
|
* ```
|
|
401
543
|
*/
|
|
402
544
|
export const notifyAppReady = (): NotifyAppReadyResult => {
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
// Oldarch returns JSON string, newarch returns array
|
|
545
|
+
const result = HotUpdaterNative.notifyAppReady();
|
|
546
|
+
// Older Android old-arch implementations returned JSON strings.
|
|
406
547
|
if (typeof result === "string") {
|
|
407
548
|
try {
|
|
408
|
-
return JSON.parse(result);
|
|
549
|
+
return normalizeNotifyAppReadyResult(JSON.parse(result));
|
|
409
550
|
} catch {
|
|
410
551
|
return { status: "STABLE" };
|
|
411
552
|
}
|
|
412
553
|
}
|
|
413
|
-
return result;
|
|
554
|
+
return normalizeNotifyAppReadyResult(result);
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
const normalizeNotifyAppReadyResult = (
|
|
558
|
+
result: NotifyAppReadyResult | { status?: string; crashedBundleId?: string },
|
|
559
|
+
): NotifyAppReadyResult => {
|
|
560
|
+
if (result.status === "RECOVERED") {
|
|
561
|
+
return {
|
|
562
|
+
status: "RECOVERED",
|
|
563
|
+
crashedBundleId: result.crashedBundleId,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
return { status: "STABLE" };
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
const createEmptyManifest = (): Manifest => ({
|
|
571
|
+
bundleId: getBundleId(),
|
|
572
|
+
assets: {},
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
const normalizeManifest = (value: unknown): Manifest => {
|
|
576
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
577
|
+
return createEmptyManifest();
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const bundleIdValue = (value as { bundleId?: unknown }).bundleId;
|
|
581
|
+
const bundleId =
|
|
582
|
+
typeof bundleIdValue === "string" && bundleIdValue.trim()
|
|
583
|
+
? bundleIdValue.trim()
|
|
584
|
+
: getBundleId();
|
|
585
|
+
|
|
586
|
+
return {
|
|
587
|
+
bundleId,
|
|
588
|
+
assets: normalizeManifestAssets((value as { assets?: unknown }).assets),
|
|
589
|
+
};
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
const normalizeManifestAssets = (value: unknown): Manifest["assets"] => {
|
|
593
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
594
|
+
return {};
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
return Object.fromEntries(
|
|
598
|
+
Object.entries(value).flatMap(([key, entry]) => {
|
|
599
|
+
const trimmedKey = key.trim();
|
|
600
|
+
|
|
601
|
+
if (!trimmedKey) {
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (typeof entry === "string") {
|
|
606
|
+
const fileHash = entry.trim();
|
|
607
|
+
|
|
608
|
+
if (!fileHash) {
|
|
609
|
+
return [];
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return [[trimmedKey, { fileHash }] as const];
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
|
|
616
|
+
return [];
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
const { fileHash } = entry as { fileHash?: unknown };
|
|
620
|
+
if (typeof fileHash !== "string" || !fileHash.trim()) {
|
|
621
|
+
return [];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
return [[trimmedKey, { fileHash: fileHash.trim() }] as const];
|
|
625
|
+
}),
|
|
626
|
+
);
|
|
414
627
|
};
|
|
415
628
|
|
|
416
629
|
/**
|
|
@@ -421,7 +634,7 @@ export const notifyAppReady = (): NotifyAppReadyResult => {
|
|
|
421
634
|
*/
|
|
422
635
|
export const getCrashHistory = (): string[] => {
|
|
423
636
|
const result = HotUpdaterNative.getCrashHistory();
|
|
424
|
-
//
|
|
637
|
+
// Older Android old-arch implementations returned JSON strings.
|
|
425
638
|
if (typeof result === "string") {
|
|
426
639
|
try {
|
|
427
640
|
return JSON.parse(result);
|
|
@@ -450,11 +663,15 @@ export const clearCrashHistory = (): boolean => {
|
|
|
450
663
|
* @returns {string | null} Base URL string (e.g., "file:///data/.../bundle-store/abc123") or null if not available
|
|
451
664
|
*/
|
|
452
665
|
export const getBaseURL = (): string | null => {
|
|
453
|
-
const
|
|
454
|
-
if (
|
|
455
|
-
return
|
|
666
|
+
const cachedBaseURL = sessionState.getCachedBaseURL();
|
|
667
|
+
if (cachedBaseURL !== undefined) {
|
|
668
|
+
return cachedBaseURL;
|
|
456
669
|
}
|
|
457
|
-
|
|
670
|
+
|
|
671
|
+
const result = HotUpdaterNative.getBaseURL();
|
|
672
|
+
const baseURL = typeof result === "string" && result !== "" ? result : null;
|
|
673
|
+
sessionState.cacheBaseURL(baseURL);
|
|
674
|
+
return baseURL;
|
|
458
675
|
};
|
|
459
676
|
|
|
460
677
|
/**
|
|
@@ -471,3 +688,32 @@ export const resetChannel = async (): Promise<boolean> => {
|
|
|
471
688
|
}
|
|
472
689
|
return ok;
|
|
473
690
|
};
|
|
691
|
+
|
|
692
|
+
/**
|
|
693
|
+
* Sets the persisted cohort used for update checks.
|
|
694
|
+
*
|
|
695
|
+
* HotUpdater only derives a device-based cohort when nothing has been stored
|
|
696
|
+
* yet. If you need to restore that initial value later, read it with
|
|
697
|
+
* `getCohort()` before calling `setCohort()`, then store it yourself.
|
|
698
|
+
*/
|
|
699
|
+
export const setCohort = (cohort: string): void => {
|
|
700
|
+
const normalized = normalizeAndValidateCohort(cohort);
|
|
701
|
+
HotUpdaterNative.setCohort(normalized);
|
|
702
|
+
sessionState.cacheCohort(normalized);
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Gets the persisted cohort used for rollout calculations.
|
|
707
|
+
* If none has been stored yet, native derives the initial value once and
|
|
708
|
+
* persists it before returning.
|
|
709
|
+
*/
|
|
710
|
+
export const getCohort = (): string => {
|
|
711
|
+
const cachedCohort = sessionState.getCachedCohort();
|
|
712
|
+
if (cachedCohort !== undefined) {
|
|
713
|
+
return cachedCohort;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const cohort = normalizeAndValidateCohort(HotUpdaterNative.getCohort());
|
|
717
|
+
sessionState.cacheCohort(cohort);
|
|
718
|
+
return cohort;
|
|
719
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { TurboModule } from "react-native";
|
|
2
2
|
import { TurboModuleRegistry } from "react-native";
|
|
3
|
+
import type { UnsafeObject } from "react-native/Libraries/Types/CodegenTypes";
|
|
3
4
|
|
|
4
5
|
export interface UpdateBundleParams {
|
|
5
6
|
bundleId: string;
|
|
@@ -60,18 +61,16 @@ export interface Spec extends TurboModule {
|
|
|
60
61
|
updateBundle(params: UpdateBundleParams): Promise<boolean>;
|
|
61
62
|
|
|
62
63
|
/**
|
|
63
|
-
*
|
|
64
|
-
*
|
|
64
|
+
* Reads the launch report for the current process.
|
|
65
|
+
* This is a read-only API; native launch state has already been finalized.
|
|
65
66
|
*
|
|
66
|
-
* @param params - Parameters containing the bundle ID
|
|
67
67
|
* @returns Object with status and optional crashedBundleId
|
|
68
|
-
* - `status: "PROMOTED"` - Staging bundle was promoted to stable (ACTIVE event)
|
|
69
68
|
* - `status: "RECOVERED"` - App recovered from crash, rollback occurred (ROLLBACK event)
|
|
70
69
|
* - `status: "STABLE"` - No changes, already stable
|
|
71
70
|
* - `crashedBundleId` - Present only when status is "RECOVERED"
|
|
72
71
|
*/
|
|
73
|
-
notifyAppReady(
|
|
74
|
-
status: "
|
|
72
|
+
notifyAppReady(): {
|
|
73
|
+
status: "RECOVERED" | "STABLE";
|
|
75
74
|
crashedBundleId?: string;
|
|
76
75
|
};
|
|
77
76
|
|
|
@@ -107,6 +106,37 @@ export interface Spec extends TurboModule {
|
|
|
107
106
|
*/
|
|
108
107
|
getBaseURL: () => string;
|
|
109
108
|
|
|
109
|
+
/**
|
|
110
|
+
* Gets the current active bundle ID from native bundle storage.
|
|
111
|
+
* Native reads the extracted bundle manifest first and falls back to the
|
|
112
|
+
* legacy BUNDLE_ID file when needed. Built-in bundle fallback is handled in JS.
|
|
113
|
+
*
|
|
114
|
+
* @returns Active bundle ID from bundle storage, or null when unavailable
|
|
115
|
+
*/
|
|
116
|
+
getBundleId: () => string | null;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Gets the current manifest from native bundle storage.
|
|
120
|
+
* Returns an empty object when manifest.json is missing or invalid.
|
|
121
|
+
*/
|
|
122
|
+
getManifest: () => UnsafeObject;
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Sets the persisted cohort used for rollout calculations.
|
|
126
|
+
*
|
|
127
|
+
* Native only derives a device-based cohort when nothing has been stored
|
|
128
|
+
* yet. Call `getCohort()` first if the app needs to save that initial value
|
|
129
|
+
* for a later restore.
|
|
130
|
+
*/
|
|
131
|
+
setCohort: (cohort: string) => void;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Gets the persisted cohort used for rollout calculations.
|
|
135
|
+
* If none has been stored yet, native derives the initial value once and
|
|
136
|
+
* persists it before returning.
|
|
137
|
+
*/
|
|
138
|
+
getCohort: () => string;
|
|
139
|
+
|
|
110
140
|
// EventEmitter
|
|
111
141
|
addListener(eventName: string): void;
|
|
112
142
|
removeListeners(count: number): void;
|
package/src/types.ts
CHANGED
|
@@ -30,6 +30,11 @@ export interface ResolverCheckUpdateParams {
|
|
|
30
30
|
*/
|
|
31
31
|
channel: string;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Cohort identifier used for server-side rollout decisions.
|
|
35
|
+
*/
|
|
36
|
+
cohort: string;
|
|
37
|
+
|
|
33
38
|
/**
|
|
34
39
|
* Update strategy being used
|
|
35
40
|
*/
|
|
@@ -57,11 +62,10 @@ export interface ResolverCheckUpdateParams {
|
|
|
57
62
|
export interface ResolverNotifyAppReadyParams {
|
|
58
63
|
/**
|
|
59
64
|
* The bundle state from native notifyAppReady
|
|
60
|
-
* - "PROMOTED": Staging bundle was promoted to stable
|
|
61
65
|
* - "RECOVERED": App recovered from crash, rollback occurred
|
|
62
66
|
* - "STABLE": No changes, bundle is stable
|
|
63
67
|
*/
|
|
64
|
-
status: "
|
|
68
|
+
status: "RECOVERED" | "STABLE";
|
|
65
69
|
|
|
66
70
|
/**
|
|
67
71
|
* Present only when status is "RECOVERED"
|
|
@@ -111,7 +115,7 @@ export interface HotUpdaterResolver {
|
|
|
111
115
|
/**
|
|
112
116
|
* Custom implementation for notifying app ready.
|
|
113
117
|
* When provided, this completely replaces the default notifyAppReady network flow.
|
|
114
|
-
* Note:
|
|
118
|
+
* Note: Native rollback/promotion semantics are already finalized before this callback runs.
|
|
115
119
|
*
|
|
116
120
|
* @param params - All parameters about the current app state
|
|
117
121
|
* @returns Notification result
|
package/src/wrap.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect,
|
|
1
|
+
import React, { useEffect, useState } from "react";
|
|
2
2
|
import { checkForUpdate } from "./checkForUpdate";
|
|
3
3
|
import type { HotUpdaterError } from "./error";
|
|
4
4
|
import { useEventCallback } from "./hooks/useEventCallback";
|
|
@@ -39,12 +39,11 @@ interface CommonHotUpdaterOptions {
|
|
|
39
39
|
requestTimeout?: number;
|
|
40
40
|
|
|
41
41
|
/**
|
|
42
|
-
* Callback invoked when the app is ready and
|
|
43
|
-
* Provides information about
|
|
42
|
+
* Callback invoked when the app is ready and the native launch report is available.
|
|
43
|
+
* Provides information about rollback recovery or stable state.
|
|
44
44
|
*
|
|
45
45
|
* @param result - Bundle state information
|
|
46
46
|
* @param result.status - Current bundle state:
|
|
47
|
-
* - "PROMOTED": Staging bundle was promoted to stable (new update applied)
|
|
48
47
|
* - "RECOVERED": App recovered from a crash, rollback occurred
|
|
49
48
|
* - "STABLE": No changes, bundle is stable
|
|
50
49
|
* @param result.crashedBundleId - Present only when status is "RECOVERED"
|
|
@@ -56,9 +55,7 @@ interface CommonHotUpdaterOptions {
|
|
|
56
55
|
* updateMode: "manual",
|
|
57
56
|
* onNotifyAppReady: ({ status, crashedBundleId }) => {
|
|
58
57
|
* if (status === "RECOVERED") {
|
|
59
|
-
* analytics.track(
|
|
60
|
-
* } else if (status === "PROMOTED") {
|
|
61
|
-
* analytics.track('bundle_promoted');
|
|
58
|
+
* analytics.track("bundle_rollback", { crashedBundleId });
|
|
62
59
|
* }
|
|
63
60
|
* }
|
|
64
61
|
* })(App);
|
|
@@ -220,7 +217,6 @@ const handleNotifyAppReady = async (options: {
|
|
|
220
217
|
onNotifyAppReady?: (result: NotifyAppReadyResult) => void;
|
|
221
218
|
}): Promise<void> => {
|
|
222
219
|
try {
|
|
223
|
-
// Always call native notifyAppReady for bundle promotion
|
|
224
220
|
const nativeResult = nativeNotifyAppReady();
|
|
225
221
|
|
|
226
222
|
// If resolver.notifyAppReady exists, call it with simplified params
|
|
@@ -249,7 +245,7 @@ export function wrap<P extends React.JSX.IntrinsicAttributes = object>(
|
|
|
249
245
|
if (options.updateMode === "manual") {
|
|
250
246
|
return (WrappedComponent: React.ComponentType<P>) => {
|
|
251
247
|
const ManualHOC: React.FC<P> = (props: P) => {
|
|
252
|
-
|
|
248
|
+
useEffect(() => {
|
|
253
249
|
void handleNotifyAppReady(options);
|
|
254
250
|
}, []);
|
|
255
251
|
|
|
@@ -342,13 +338,13 @@ export function wrap<P extends React.JSX.IntrinsicAttributes = object>(
|
|
|
342
338
|
restOptions.onProgress?.(progress);
|
|
343
339
|
}, [progress]);
|
|
344
340
|
|
|
345
|
-
//
|
|
346
|
-
|
|
341
|
+
// Read the native launch report after the first render commit.
|
|
342
|
+
useEffect(() => {
|
|
347
343
|
void handleNotifyAppReady(restOptions);
|
|
348
344
|
}, []);
|
|
349
345
|
|
|
350
346
|
// Start update check
|
|
351
|
-
|
|
347
|
+
useEffect(() => {
|
|
352
348
|
initHotUpdater();
|
|
353
349
|
}, []);
|
|
354
350
|
|