react-native-bspatch 2.0.0 → 3.0.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 (52) hide show
  1. package/README.md +126 -151
  2. package/android/src/main/java/vn/reactnativebspatch/BsPatchModule.kt +0 -25
  3. package/android/src/main/java/vn/reactnativebspatch/OtaBundleResolver.kt +14 -6
  4. package/ios/OtaBundleResolver.swift +14 -6
  5. package/ios/RNBsPatch.mm +1 -28
  6. package/lib/commonjs/NativeBsPatch.js.map +1 -1
  7. package/lib/commonjs/ota/OtaProvider.js +29 -16
  8. package/lib/commonjs/ota/OtaProvider.js.map +1 -1
  9. package/lib/commonjs/ota/otaAssetResolver.js +0 -20
  10. package/lib/commonjs/ota/otaAssetResolver.js.map +1 -1
  11. package/lib/commonjs/ota/otaCore.js +200 -330
  12. package/lib/commonjs/ota/otaCore.js.map +1 -1
  13. package/lib/commonjs/ota/otaMetadata.js +21 -21
  14. package/lib/commonjs/ota/otaMetadata.js.map +1 -1
  15. package/lib/commonjs/ota/storage/GitHubRawStorage.js +13 -15
  16. package/lib/commonjs/ota/storage/GitHubRawStorage.js.map +1 -1
  17. package/lib/commonjs/ota/types.js.map +1 -1
  18. package/lib/module/NativeBsPatch.js.map +1 -1
  19. package/lib/module/ota/OtaProvider.js +30 -17
  20. package/lib/module/ota/OtaProvider.js.map +1 -1
  21. package/lib/module/ota/otaAssetResolver.js +1 -21
  22. package/lib/module/ota/otaAssetResolver.js.map +1 -1
  23. package/lib/module/ota/otaCore.js +197 -329
  24. package/lib/module/ota/otaCore.js.map +1 -1
  25. package/lib/module/ota/otaMetadata.js +19 -19
  26. package/lib/module/ota/otaMetadata.js.map +1 -1
  27. package/lib/module/ota/storage/GitHubRawStorage.js +13 -15
  28. package/lib/module/ota/storage/GitHubRawStorage.js.map +1 -1
  29. package/lib/module/ota/types.js.map +1 -1
  30. package/lib/typescript/NativeBsPatch.d.ts +0 -1
  31. package/lib/typescript/NativeBsPatch.d.ts.map +1 -1
  32. package/lib/typescript/ota/OtaProvider.d.ts.map +1 -1
  33. package/lib/typescript/ota/otaAssetResolver.d.ts.map +1 -1
  34. package/lib/typescript/ota/otaCore.d.ts +13 -3
  35. package/lib/typescript/ota/otaCore.d.ts.map +1 -1
  36. package/lib/typescript/ota/otaMetadata.d.ts +9 -7
  37. package/lib/typescript/ota/otaMetadata.d.ts.map +1 -1
  38. package/lib/typescript/ota/storage/GitHubRawStorage.d.ts.map +1 -1
  39. package/lib/typescript/ota/types.d.ts +5 -19
  40. package/lib/typescript/ota/types.d.ts.map +1 -1
  41. package/package.json +1 -1
  42. package/src/NativeBsPatch.ts +0 -5
  43. package/src/ota/OtaProvider.tsx +30 -23
  44. package/src/ota/otaAssetResolver.ts +1 -17
  45. package/src/ota/otaCore.ts +203 -457
  46. package/src/ota/otaMetadata.ts +30 -20
  47. package/src/ota/storage/GitHubRawStorage.ts +13 -21
  48. package/src/ota/types.ts +5 -20
  49. package/tools/build_ota.sh +222 -525
  50. package/tools/setup_project.js +17 -115
  51. package/tools/update_registry.js +31 -32
  52. package/tools/upload_ota.sh +61 -14
@@ -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
- /** Promise-based lock to prevent concurrent metadata writes (double-tap guard). */
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
- * Persists OTA metadata to the local filesystem.
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 saveMetadata = async (metadata: OtaMetadata): Promise<void> => {
55
- // Wait for any in-flight save to complete before starting our own
53
+ export const updateMetadata = async (
54
+ defaultVersion: number,
55
+ fn: (metadata: OtaMetadata) => OtaMetadata,
56
+ ): Promise<OtaMetadata> => {
56
57
  const prevLock = saveLock;
57
- let settleFn: (() => void) | undefined;
58
- saveLock = new Promise<void>(resolve => { settleFn = 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
- settleFn!();
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 current session as healthy, indicating a successful boot.
159
- * This prevents the native bridge from rolling back the version on the next launch.
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 metadata = await loadMetadata(defaultVersion);
170
- const updated: OtaMetadata = {
171
- ...metadata,
172
- // Stamp legacy metadata so the native resolver can detect a store update later
173
- appVersion: metadata.appVersion ?? appVersion,
174
- lastLaunchSuccessful: true,
175
- rollbackCount: 0,
176
- };
177
- await saveMetadata(updated);
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 fetching OTA updates directly from a GitHub Raw repository.
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: All manifest/registry fetches include Cache-Control: no-cache and a
14
- * timestamp query param to bypass GitHub CDN's 5-minute (max-age=300) cache.
15
- * Asset URLs are content-addressed (hash-named) so they are immutable and
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) return config.bundledPatches || [];
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
- if (!flavorGroup?.patches?.length) {
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
- console.warn(
83
- '[GitHubRawStorage] Manifest fetch error:',
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
- // Cache-bust: patch files are versioned by name but GitHub CDN may
105
- // serve a stale 404 response for newly uploaded files within 5 min.
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
- /** Release tag (legacy, keep for type compatibility) */
39
- releaseTag?: string;
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
- /** URL for full bundle download (fallback when patch fails) */
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 */