react-native-bspatch 2.0.1 → 3.0.1
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/README.md +127 -151
- package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +0 -25
- package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +14 -6
- package/ios/OtaBundleResolver.swift +14 -6
- package/ios/RNBsPatch.mm +1 -28
- package/lib/commonjs/NativeBsPatch.js.map +1 -1
- package/lib/commonjs/ota/OtaProvider.js +29 -16
- package/lib/commonjs/ota/OtaProvider.js.map +1 -1
- package/lib/commonjs/ota/otaAssetResolver.js +0 -20
- package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
- package/lib/commonjs/ota/otaCore.js +200 -330
- package/lib/commonjs/ota/otaCore.js.map +1 -1
- package/lib/commonjs/ota/otaMetadata.js +21 -21
- package/lib/commonjs/ota/otaMetadata.js.map +1 -1
- package/lib/commonjs/ota/storage/GitHubRawStorage.js +13 -15
- package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/commonjs/ota/types.js.map +1 -1
- package/lib/module/NativeBsPatch.js.map +1 -1
- package/lib/module/ota/OtaProvider.js +30 -17
- package/lib/module/ota/OtaProvider.js.map +1 -1
- package/lib/module/ota/otaAssetResolver.js +1 -21
- package/lib/module/ota/otaAssetResolver.js.map +1 -1
- package/lib/module/ota/otaCore.js +197 -329
- package/lib/module/ota/otaCore.js.map +1 -1
- package/lib/module/ota/otaMetadata.js +19 -19
- package/lib/module/ota/otaMetadata.js.map +1 -1
- package/lib/module/ota/storage/GitHubRawStorage.js +13 -15
- package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
- package/lib/module/ota/types.js.map +1 -1
- package/lib/typescript/NativeBsPatch.d.ts +0 -1
- package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
- package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
- package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
- package/lib/typescript/ota/otaCore.d.ts +13 -3
- package/lib/typescript/ota/otaCore.d.ts.map +1 -1
- package/lib/typescript/ota/otaMetadata.d.ts +9 -7
- package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
- package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
- package/lib/typescript/ota/types.d.ts +5 -19
- package/lib/typescript/ota/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/NativeBsPatch.ts +0 -5
- package/src/ota/OtaProvider.tsx +30 -23
- package/src/ota/otaAssetResolver.ts +1 -17
- package/src/ota/otaCore.ts +203 -457
- package/src/ota/otaMetadata.ts +30 -20
- package/src/ota/storage/GitHubRawStorage.ts +13 -21
- package/src/ota/types.ts +5 -20
- package/tools/build_ota.sh +206 -516
- package/tools/cli.js +9 -0
- package/tools/setup_project.js +17 -115
- package/tools/update_registry.js +31 -32
- package/tools/upload_ota.sh +61 -14
package/src/ota/otaMetadata.ts
CHANGED
|
@@ -6,7 +6,7 @@ const OTA_DIR = `${ReactNativeBlobUtil.fs.dirs.DocumentDir}/ota`;
|
|
|
6
6
|
const METADATA_PATH = `${OTA_DIR}/metadata.json`;
|
|
7
7
|
const METADATA_TMP_PATH = `${METADATA_PATH}.tmp`;
|
|
8
8
|
|
|
9
|
-
/**
|
|
9
|
+
/** Serializes metadata updates (see updateMetadata). */
|
|
10
10
|
let saveLock: Promise<void> = Promise.resolve();
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -47,18 +47,20 @@ export const loadMetadata = async (
|
|
|
47
47
|
};
|
|
48
48
|
|
|
49
49
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* @param metadata - The metadata object to save.
|
|
50
|
+
* Atomically updates metadata: load → fn → save, serialized with every other
|
|
51
|
+
* update so a background install and markHealthy can't overwrite each other.
|
|
53
52
|
*/
|
|
54
|
-
export const
|
|
55
|
-
|
|
53
|
+
export const updateMetadata = async (
|
|
54
|
+
defaultVersion: number,
|
|
55
|
+
fn: (metadata: OtaMetadata) => OtaMetadata,
|
|
56
|
+
): Promise<OtaMetadata> => {
|
|
56
57
|
const prevLock = saveLock;
|
|
57
|
-
let
|
|
58
|
-
saveLock = new Promise<void>(resolve =>
|
|
58
|
+
let release!: () => void;
|
|
59
|
+
saveLock = new Promise<void>(resolve => (release = resolve));
|
|
59
60
|
|
|
60
61
|
try {
|
|
61
62
|
await prevLock;
|
|
63
|
+
const metadata = fn(await loadMetadata(defaultVersion));
|
|
62
64
|
await ensureOtaDir();
|
|
63
65
|
// Write-then-rename so a kill mid-write never leaves truncated JSON
|
|
64
66
|
await ReactNativeBlobUtil.fs.writeFile(
|
|
@@ -70,11 +72,12 @@ export const saveMetadata = async (metadata: OtaMetadata): Promise<void> => {
|
|
|
70
72
|
await ReactNativeBlobUtil.fs.unlink(METADATA_PATH);
|
|
71
73
|
}
|
|
72
74
|
await ReactNativeBlobUtil.fs.mv(METADATA_TMP_PATH, METADATA_PATH);
|
|
75
|
+
return metadata;
|
|
73
76
|
} catch (e) {
|
|
74
77
|
console.error('[otaMetadata] Failed to save metadata:', e);
|
|
75
78
|
throw new Error(`Failed to save metadata: ${(e as Error).message}`);
|
|
76
79
|
} finally {
|
|
77
|
-
|
|
80
|
+
release();
|
|
78
81
|
}
|
|
79
82
|
};
|
|
80
83
|
|
|
@@ -155,26 +158,33 @@ export const cleanupOldVersions = async (
|
|
|
155
158
|
};
|
|
156
159
|
|
|
157
160
|
/**
|
|
158
|
-
* Marks the
|
|
159
|
-
*
|
|
161
|
+
* Marks the running version healthy: the native resolver stops treating its
|
|
162
|
+
* launches as pending, so it is never rolled back afterwards (crash on first
|
|
163
|
+
* launch → rollback; crash after a verified launch → app bug, not a bad bundle).
|
|
160
164
|
* Also cleans up obsolete version directories from disk.
|
|
161
165
|
*
|
|
162
166
|
* @param defaultVersion - Base version for metadata lookup fallback.
|
|
167
|
+
* @param runningVersion - Version loaded at boot. If a background install has
|
|
168
|
+
* since switched currentVersion, the new version hasn't run yet and is left pending.
|
|
163
169
|
* @returns The updated metadata.
|
|
164
170
|
*/
|
|
165
171
|
export const markHealthy = async (
|
|
166
172
|
defaultVersion: number,
|
|
173
|
+
runningVersion: number,
|
|
167
174
|
appVersion?: string,
|
|
168
175
|
): Promise<OtaMetadata> => {
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
const updated = await updateMetadata(defaultVersion, metadata =>
|
|
177
|
+
metadata.currentVersion !== runningVersion
|
|
178
|
+
? metadata
|
|
179
|
+
: {
|
|
180
|
+
...metadata,
|
|
181
|
+
// Stamp legacy metadata so the native resolver can detect a store update later
|
|
182
|
+
appVersion: metadata.appVersion ?? appVersion,
|
|
183
|
+
lastLaunchSuccessful: true,
|
|
184
|
+
rollbackCount: 0,
|
|
185
|
+
verifiedVersion: runningVersion,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
178
188
|
|
|
179
189
|
// Garbage collect older versions on disk to free storage
|
|
180
190
|
await cleanupOldVersions(updated, defaultVersion);
|
|
@@ -3,17 +3,17 @@ import ReactNativeBlobUtil from 'react-native-blob-util';
|
|
|
3
3
|
import type { OtaConfig, OtaPatch, OtaStorageProvider } from '../types';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Storage Provider for
|
|
6
|
+
* Storage Provider for any static host with this layout: GitHub raw, a public
|
|
7
|
+
* Cloudflare R2 bucket (r2.dev or custom domain), S3, a CDN...
|
|
7
8
|
*
|
|
8
9
|
* Direct URL mapping:
|
|
9
10
|
* - Manifest: `${baseUrl}/${baseFolder}/ota_registry.json`
|
|
10
11
|
* - Patch file: `${baseUrl}/${baseFolder}/${appVersion}/${channel}/${filename}`
|
|
11
12
|
* - Assets: `${baseUrl}/${baseFolder}/assets/${hash}.${ext}`
|
|
12
13
|
*
|
|
13
|
-
* NOTE:
|
|
14
|
-
* timestamp query param
|
|
15
|
-
*
|
|
16
|
-
* intentionally NOT cache-busted.
|
|
14
|
+
* NOTE: Only the registry is mutable: it is fetched with Cache-Control: no-cache
|
|
15
|
+
* and a timestamp query param (GitHub's CDN caches for 5 minutes). Bundles,
|
|
16
|
+
* patches, manifests and assets are never overwritten, so they stay cacheable.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
/** Headers to bypass GitHub CDN cache on mutable files (registry, manifests) */
|
|
@@ -32,7 +32,9 @@ const normalizeChannel = (channel: string): string => {
|
|
|
32
32
|
|
|
33
33
|
export class GitHubRawStorageProvider implements OtaStorageProvider {
|
|
34
34
|
async fetchManifest(config: OtaConfig): Promise<OtaPatch[]> {
|
|
35
|
-
if (!config.customServer)
|
|
35
|
+
if (!config.customServer) {
|
|
36
|
+
throw new Error('[GitHubRawStorage] customServer configuration missing');
|
|
37
|
+
}
|
|
36
38
|
|
|
37
39
|
const { baseUrl, channel, baseFolder = 'ota' } = config.customServer;
|
|
38
40
|
const channelName = normalizeChannel(channel);
|
|
@@ -69,21 +71,14 @@ export class GitHubRawStorageProvider implements OtaStorageProvider {
|
|
|
69
71
|
const appGroup = registry?.apps?.[config.appVersion];
|
|
70
72
|
const flavorGroup = appGroup?.flavors?.[channelName];
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
return config.bundledPatches || [];
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return (flavorGroup.patches || []).filter(
|
|
74
|
+
return (flavorGroup?.patches ?? []).filter(
|
|
77
75
|
(p: OtaPatch) =>
|
|
78
76
|
!p.platform ||
|
|
79
77
|
p.platform.toLowerCase() === Platform.OS.toLowerCase(),
|
|
80
78
|
);
|
|
81
79
|
} catch (error) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
(error as Error).message,
|
|
85
|
-
);
|
|
86
|
-
return config.bundledPatches || [];
|
|
80
|
+
// Surface it: "no patches" would report a broken registry as up to date
|
|
81
|
+
throw new Error(`[GitHubRawStorage] Manifest fetch error: ${(error as Error).message}`);
|
|
87
82
|
}
|
|
88
83
|
}
|
|
89
84
|
|
|
@@ -101,11 +96,8 @@ export class GitHubRawStorageProvider implements OtaStorageProvider {
|
|
|
101
96
|
: baseUrl;
|
|
102
97
|
const channelName = normalizeChannel(channel);
|
|
103
98
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
return bustCache(
|
|
107
|
-
`${normalizedBaseUrl}/${baseFolder}/${config.appVersion}/${channelName}/${filename}`,
|
|
108
|
-
);
|
|
99
|
+
// Versioned names, never overwritten: no cache-bust, so a CDN can cache them
|
|
100
|
+
return `${normalizedBaseUrl}/${baseFolder}/${config.appVersion}/${channelName}/${filename}`;
|
|
109
101
|
}
|
|
110
102
|
|
|
111
103
|
async getAssetUrl(
|
package/src/ota/types.ts
CHANGED
|
@@ -35,10 +35,8 @@ export type OtaPatch = {
|
|
|
35
35
|
label: string;
|
|
36
36
|
/** App version this patch belongs to */
|
|
37
37
|
appVersion: string;
|
|
38
|
-
/**
|
|
39
|
-
|
|
40
|
-
/** Filename on the server (e.g. "v1-to-v3.patch") */
|
|
41
|
-
filename: string;
|
|
38
|
+
/** Patch file on the server; absent on the full-bundle entry (installs `fullBundleUrl`) */
|
|
39
|
+
filename?: string;
|
|
42
40
|
/** Version the app must be at to apply this patch */
|
|
43
41
|
fromVersion: number;
|
|
44
42
|
/** Version this patch upgrades to */
|
|
@@ -49,14 +47,12 @@ export type OtaPatch = {
|
|
|
49
47
|
bundleHash?: string;
|
|
50
48
|
/** Minimum native app version required (semver) */
|
|
51
49
|
minAppVersion?: string;
|
|
52
|
-
/**
|
|
50
|
+
/** Full bundle file: used when there is no OTA bundle to patch from, or the patch fails */
|
|
53
51
|
fullBundleUrl?: string;
|
|
54
52
|
/** RSA/ECDSA SHA-256 signature (base64) of the patch file */
|
|
55
53
|
signature?: string;
|
|
56
54
|
/** RSA/ECDSA SHA-256 signature (base64) of the resulting bundle */
|
|
57
55
|
bundleSignature?: string;
|
|
58
|
-
/** Whether this update is mandatory */
|
|
59
|
-
mandatory?: boolean;
|
|
60
56
|
/** URL to the asset manifest JSON for this version */
|
|
61
57
|
assetManifestUrl?: string;
|
|
62
58
|
/** Target platform for this patch */
|
|
@@ -77,15 +73,6 @@ export type OtaAsset = {
|
|
|
77
73
|
originalPath: string;
|
|
78
74
|
};
|
|
79
75
|
|
|
80
|
-
export type OtaRemotePatch = {
|
|
81
|
-
version: number;
|
|
82
|
-
minAppVersion: string;
|
|
83
|
-
patchUrl: string;
|
|
84
|
-
bundleHash: string;
|
|
85
|
-
baseHash: string;
|
|
86
|
-
mandatory: boolean;
|
|
87
|
-
};
|
|
88
|
-
|
|
89
76
|
export type OtaMetadata = {
|
|
90
77
|
currentVersion: number;
|
|
91
78
|
previousVersions: number[];
|
|
@@ -95,6 +82,8 @@ export type OtaMetadata = {
|
|
|
95
82
|
appVersion?: string;
|
|
96
83
|
/** Versions that crashed on boot and were rolled back; never re-applied */
|
|
97
84
|
failedVersions?: number[];
|
|
85
|
+
/** Last version that launched and rendered successfully; the native resolver never rolls it back */
|
|
86
|
+
verifiedVersion?: number;
|
|
98
87
|
};
|
|
99
88
|
|
|
100
89
|
export type OtaMetrics = {
|
|
@@ -108,7 +97,6 @@ export type OtaMetrics = {
|
|
|
108
97
|
export type OtaNetworkStrategy = {
|
|
109
98
|
retry?: number;
|
|
110
99
|
timeoutMs?: number;
|
|
111
|
-
wifiOnly?: boolean;
|
|
112
100
|
};
|
|
113
101
|
|
|
114
102
|
export type OtaConfig = {
|
|
@@ -116,8 +104,6 @@ export type OtaConfig = {
|
|
|
116
104
|
appVersion?: string;
|
|
117
105
|
/** Initial version baked into the APK (usually 1) */
|
|
118
106
|
baseAppVersion: number;
|
|
119
|
-
/** Default patches if manifest fetch fails */
|
|
120
|
-
bundledPatches?: OtaPatch[];
|
|
121
107
|
/**
|
|
122
108
|
* Allow OTA checks and patch application while React Native is running in
|
|
123
109
|
* development mode (`__DEV__`). Disabled by default for safety.
|
|
@@ -126,7 +112,6 @@ export type OtaConfig = {
|
|
|
126
112
|
|
|
127
113
|
/** Enterprise configurations */
|
|
128
114
|
updateMode?: OtaUpdateMode;
|
|
129
|
-
maxRollback?: number;
|
|
130
115
|
network?: OtaNetworkStrategy;
|
|
131
116
|
|
|
132
117
|
/** Telemetry: structured event callback for analytics */
|