react-native-update 10.48.2 → 10.48.3
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-CN.md +40 -4
- package/README.md +40 -4
- package/android/src/main/java/cn/reactnative/modules/update/UpdateEventEmitter.java +13 -4
- package/biome.json +59 -0
- package/harmony/pushy/src/main/ets/DownloadTask.ts +2 -2
- package/harmony/pushy.har +0 -0
- package/package.json +5 -6
- package/src/NativePushy.ts +1 -1
- package/src/client.ts +62 -54
- package/src/context.ts +7 -7
- package/src/core.ts +51 -51
- package/src/endpoint.ts +7 -7
- package/src/error.ts +3 -3
- package/src/i18n.ts +2 -2
- package/src/index.ts +4 -4
- package/src/isInRollout.ts +10 -1
- package/src/provider.tsx +46 -56
- package/src/resolveCheckResult.ts +1 -1
- package/src/telemetry.ts +1 -1
- package/src/type.ts +2 -2
- package/src/utils.ts +30 -30
- package/.eslintrc.js +0 -4
- package/.prettierrc.js +0 -6
package/README-CN.md
CHANGED
|
@@ -19,13 +19,45 @@
|
|
|
19
19
|
### 优势
|
|
20
20
|
|
|
21
21
|
1. 对中国用户使用阿里云高速 CDN 分发,对比其他服务器在国外的热更新服务,分发更稳定,更新成功率极高。海外用户智能分流至 cloudflare,同样提供稳定高速的分发体验。
|
|
22
|
-
2. 基于 bsdiff/hdiff 算法创建的**超小更新包**,通常版本迭代后在几十至几百 KB 级别(其他全量热更新服务所需流量通常在几十 MB
|
|
22
|
+
2. 基于 bsdiff/hdiff 算法创建的**超小更新包**,通常版本迭代后在几十至几百 KB 级别(其他全量热更新服务所需流量通常在几十 MB 级别)。更针对 **Hermes 字节码专门优化了 diff 算法**(详见下方[算法对比](#diff-算法对比)),补丁体积进一步显著缩小。
|
|
23
23
|
3. 始终跟进 RN 最新正式版本,第一时间提供支持。支持 hermes 字节码格式。支持新架构(注:安卓 0.73.0 ~ 0.76.0 的新架构因官方 bug 不支持,0.73 以下或 0.76.1 以上的新架构可用)。
|
|
24
24
|
4. 跨越多个版本进行更新时,只需要下载**一个更新包**,不需要逐版本依次更新。
|
|
25
25
|
5. 命令行工具 & 网页双端管理,版本发布过程简单便捷,完全可以集成 CI。
|
|
26
|
-
6.
|
|
27
|
-
7.
|
|
28
|
-
8.
|
|
26
|
+
6. 后台自带**数据统计**(版本分布、更新成功率等)、**灰度发布**(按比例逐步放量)与**版本健康度监控**,无需自建数据链路即可掌控每次发布。
|
|
27
|
+
7. 支持崩溃回滚,安全可靠,结合健康度监控可及时发现并止损问题版本。
|
|
28
|
+
8. meta 信息及开放 API,提供更高扩展性。
|
|
29
|
+
9. 提供付费的专人技术支持。
|
|
30
|
+
|
|
31
|
+
### Diff 算法对比
|
|
32
|
+
|
|
33
|
+
传统 diff 算法直接对字节做差分,而 Hermes 字节码中大量结构是"偏移量表"——JS 一处小改动会令其后所有偏移量整体位移,导致二进制差异被急剧放大。我们在 hdiff 之前引入了一层**针对 HBC(Hermes 字节码)的 delta-friendly 可逆变换**:对偏移位域做前项差分,从根源上消除这种位移放大。布局描述表由发布端随补丁下发,Hermes 版本演进时**客户端零升级**、自动兼容。
|
|
34
|
+
|
|
35
|
+
以下数据在真实 RN 0.86 应用的 release 产物上实测(Hermes HBC v98,字节码约 4.4 MB),每个补丁都先通过完整往返校验再记录体积。完整的评测方法、测试数据与可复现代码见 **[hbc-diff-benchmark](https://github.com/sunnylqm/hbc-diff-benchmark)**。
|
|
36
|
+
|
|
37
|
+
**Hermes 字节码(.hbc)——生产环境 Hermes 应用的实际发包格式:**
|
|
38
|
+
|
|
39
|
+
| 迭代场景 | 全量 OTA(gzip) | bsdiff | bsdiff+lzma | hdiff | hdiff + HBC 变换 |
|
|
40
|
+
|---|---|---|---|---|---|
|
|
41
|
+
| 单行文案修改 | 1901.5 KB | 93.7 KB | 87.7 KB | 89.0 KB | **63.5 KB**(−28% / −29%) |
|
|
42
|
+
| 小功能(约 60 行) | 1913.9 KB | 411.6 KB | 330.9 KB | 328.0 KB | **285.5 KB**(−14% / −13%) |
|
|
43
|
+
| 中等功能(约 300 行) | 1973.7 KB | 551.6 KB | 430.6 KB | 431.6 KB | **398.4 KB**(−7% / −8%) |
|
|
44
|
+
|
|
45
|
+
**文本 JS bundle——非 Hermes 应用(同一应用、同样三个场景):**
|
|
46
|
+
|
|
47
|
+
| 迭代场景 | 全量 OTA(gzip) | bsdiff | bsdiff+lzma | hdiff |
|
|
48
|
+
|---|---|---|---|---|
|
|
49
|
+
| 单行文案修改 | 807.2 KB | 0.3 KB | 0.7 KB | **0.1 KB** |
|
|
50
|
+
| 小功能(约 60 行) | 813.1 KB | 7.4 KB | 7.4 KB | **5.8 KB** |
|
|
51
|
+
| 中等功能(约 300 行) | 837.7 KB | 38.4 KB | 37.4 KB | **28.7 KB** |
|
|
52
|
+
|
|
53
|
+
文本 bundle 没有偏移量表,diff 天然干净——补丁只有几百字节到几十 KB,较大改动下 hdiff 仍比 bsdiff 小约 20%~25%。偏移位移放大是 Hermes 字节码特有的问题,这正是 HBC 变换存在的意义。
|
|
54
|
+
|
|
55
|
+
要点:
|
|
56
|
+
|
|
57
|
+
- 增量更新比全量 OTA 节省 **5~30 倍**流量(单行改动 63.5 KB vs 全量 1.9 MB)。
|
|
58
|
+
- bsdiff+lzma 对照组(同一份 bsdiff 差分数据、换用 lzma 压缩)表明压缩器占了 bsdiff 与 hdiff 差距的大头——而 HBC 针对性变换再带来的 **7%~29%** 缩减是**纯算法收益**,且越是高频的小改动收益越大。
|
|
59
|
+
- hdiff 生成补丁比 bsdiff 快 **2~4 倍**,变换本身开销仅个位数毫秒。
|
|
60
|
+
- 安全兜底:改写任何字节前先对段边界、位域范围做全量校验,不匹配时自动回退到普通 hdiff 路径。
|
|
29
61
|
|
|
30
62
|
### 与其他热更新库对比
|
|
31
63
|
|
|
@@ -42,6 +74,10 @@
|
|
|
42
74
|
| **新架构支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
|
|
43
75
|
| **Hermes 支持** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
|
|
44
76
|
| **崩溃回滚** | ✅ 自动回滚机制 | ✅ 支持 | ❌ **已停运** |
|
|
77
|
+
| **灰度发布** | ✅ 后台自带,按比例逐步放量 | ✅ 支持 | ❌ **已停运** |
|
|
78
|
+
| **数据统计** | ✅ 后台自带(版本分布、更新成功率等) | ⚠️ 有限 | ❌ **已停运** |
|
|
79
|
+
| **健康度监控** | ✅ 后台自带版本健康度监控 | ❌ 不支持 | ❌ **已停运** |
|
|
80
|
+
| **Hermes 专项 diff 优化** | ✅ HBC 结构感知,补丁更小 | ❌ 全量更新 | ❌ **已停运** |
|
|
45
81
|
| **管理界面** | ✅ 命令行工具 + Web 管理界面 | ✅ Expo Dashboard | ❌ **已停运** |
|
|
46
82
|
| **CI/CD 集成** | ✅ 支持 | ✅ 支持 | ❌ **已停运** |
|
|
47
83
|
| **API 扩展性** | ✅ Meta 信息 + 开放 API | ⚠️ 有限 | ❌ **已停运** |
|
package/README.md
CHANGED
|
@@ -17,13 +17,45 @@ See the docs:
|
|
|
17
17
|
## Advantages
|
|
18
18
|
|
|
19
19
|
1. react-native-update provides a dedicated global service with fast and reliable worldwide delivery.
|
|
20
|
-
2. **Tiny update packages** generated with bsdiff/hdiff are typically only tens to hundreds of KB, instead of the tens of MB usually required by full-bundle update systems.
|
|
20
|
+
2. **Tiny update packages** generated with bsdiff/hdiff are typically only tens to hundreds of KB, instead of the tens of MB usually required by full-bundle update systems. On top of that, the diff algorithm is **specifically optimized for Hermes bytecode** (see the [comparison below](#diff-algorithm-comparison)), shrinking patches even further.
|
|
21
21
|
3. The library tracks new React Native stable releases closely, supports Hermes bytecode, and supports the new architecture. Note: Android RN 0.73.0 to 0.76.0 new architecture is unavailable because of upstream issues; versions below 0.73 or above 0.76.1 are supported.
|
|
22
22
|
4. When updating across multiple versions, clients only need to download **one update package** instead of applying every intermediate version in sequence.
|
|
23
23
|
5. Command-line tools and a web dashboard are both available, making release workflows simple and CI-friendly.
|
|
24
|
-
6.
|
|
25
|
-
7.
|
|
26
|
-
8.
|
|
24
|
+
6. The dashboard ships with **built-in analytics** (version distribution, update success rate), **staged (gray) rollouts** by percentage, and **release health monitoring** — no need to build your own data pipeline to keep every release under control.
|
|
25
|
+
7. Built-in crash rollback keeps updates safe and reliable, and health monitoring helps you catch and stop a bad release early.
|
|
26
|
+
8. Meta information and open APIs make the system more extensible.
|
|
27
|
+
9. Paid technical support is available.
|
|
28
|
+
|
|
29
|
+
## Diff Algorithm Comparison
|
|
30
|
+
|
|
31
|
+
Traditional diff algorithms operate on raw bytes. Hermes bytecode, however, is full of offset tables — a small JS change shifts every subsequent offset, which dramatically amplifies the binary difference. Before running hdiff, we apply a **delta-friendly reversible transform tailored to HBC (Hermes bytecode)**: offset bitfields are delta-encoded so the offset-shift amplification disappears at the source. The layout description table is shipped with each patch, so when Hermes evolves, **clients need zero upgrades** — compatibility is automatic.
|
|
32
|
+
|
|
33
|
+
The numbers below are measured on real release bundles of a React Native 0.86 app (Hermes HBC v98, ~4.4 MB bytecode); every patch is verified by an actual round-trip before its size is recorded. Full methodology, fixtures, and runnable code: **[hbc-diff-benchmark](https://github.com/sunnylqm/hbc-diff-benchmark)**.
|
|
34
|
+
|
|
35
|
+
**Hermes bytecode (.hbc) — what ships in production Hermes apps:**
|
|
36
|
+
|
|
37
|
+
| Scenario | Full OTA (gzip) | bsdiff | bsdiff+lzma | hdiff | hdiff + HBC transform |
|
|
38
|
+
|---|---|---|---|---|---|
|
|
39
|
+
| One-line text change | 1901.5 KB | 93.7 KB | 87.7 KB | 89.0 KB | **63.5 KB** (−28% / −29%) |
|
|
40
|
+
| Small feature (~60 LOC) | 1913.9 KB | 411.6 KB | 330.9 KB | 328.0 KB | **285.5 KB** (−14% / −13%) |
|
|
41
|
+
| Medium feature (~300 LOC) | 1973.7 KB | 551.6 KB | 430.6 KB | 431.6 KB | **398.4 KB** (−7% / −8%) |
|
|
42
|
+
|
|
43
|
+
**Plain-text JS bundle — non-Hermes apps (same scenarios, same app):**
|
|
44
|
+
|
|
45
|
+
| Scenario | Full OTA (gzip) | bsdiff | bsdiff+lzma | hdiff |
|
|
46
|
+
|---|---|---|---|---|
|
|
47
|
+
| One-line text change | 807.2 KB | 0.3 KB | 0.7 KB | **0.1 KB** |
|
|
48
|
+
| Small feature (~60 LOC) | 813.1 KB | 7.4 KB | 7.4 KB | **5.8 KB** |
|
|
49
|
+
| Medium feature (~300 LOC) | 837.7 KB | 38.4 KB | 37.4 KB | **28.7 KB** |
|
|
50
|
+
|
|
51
|
+
Text bundles have no offset tables, so they diff cleanly without any transform — patches are hundreds of bytes to tens of KB, and hdiff still beats bsdiff by ~20–25% on the larger changes. The offset-shift amplification is a Hermes-bytecode-specific problem, which is exactly why the HBC transform exists.
|
|
52
|
+
|
|
53
|
+
Highlights:
|
|
54
|
+
|
|
55
|
+
- Incremental updates save **5–30×** bandwidth compared to full-bundle OTA (63.5 KB vs 1.9 MB for a one-line change).
|
|
56
|
+
- The bsdiff+lzma control group (same bsdiff delta, lzma compression) shows the compressor accounts for most of the bsdiff-vs-hdiff gap — the further **7–29%** cut from the HBC-aware transform is **pure algorithmic gain**, largest on the small, frequent updates that dominate real OTA traffic.
|
|
57
|
+
- Patch generation with hdiff is **2–4× faster** than bsdiff; the transform itself costs single-digit milliseconds.
|
|
58
|
+
- Safety first: section bounds and bitfield ranges are fully validated before any byte is touched, and the pipeline automatically falls back to plain hdiff on any mismatch.
|
|
27
59
|
|
|
28
60
|
## Comparison With Other OTA Libraries
|
|
29
61
|
|
|
@@ -31,6 +63,10 @@ See the docs:
|
|
|
31
63
|
|---------|---------------------|-------------|------------------------|
|
|
32
64
|
| **Price / Cost** | Free tier with multiple paid plans, bandwidth included | Free tier with multiple paid plans, extra bandwidth charges apply | ❌ **Discontinued** (Microsoft App Center shut down on March 31, 2025) |
|
|
33
65
|
| **Package Size** | ⭐⭐⭐⭐⭐ Tens to hundreds of KB (incremental) | ⭐⭐⭐ Full bundle updates (usually tens of MB) | ❌ **Discontinued** |
|
|
66
|
+
| **Hermes-specific diff** | ✅ HBC-aware transform, smallest patches | ❌ Full bundle | ❌ **Discontinued** |
|
|
67
|
+
| **Staged (gray) rollout** | ✅ Built-in, percentage-based | ✅ Supported | ❌ **Discontinued** |
|
|
68
|
+
| **Analytics** | ✅ Built-in (version distribution, success rate) | ⚠️ Limited | ❌ **Discontinued** |
|
|
69
|
+
| **Release health monitoring** | ✅ Built-in | ❌ Not available | ❌ **Discontinued** |
|
|
34
70
|
| **Technical Support** | ✅ Paid dedicated support | ⚠️ Community support | ❌ **Discontinued** |
|
|
35
71
|
| **Server Deployment** | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ **Discontinued** |
|
|
36
72
|
| **Bandwidth Usage** | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ **Discontinued** |
|
|
@@ -6,6 +6,7 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
6
6
|
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
7
7
|
import com.facebook.react.bridge.WritableMap;
|
|
8
8
|
import java.lang.ref.WeakReference;
|
|
9
|
+
import java.lang.reflect.Method;
|
|
9
10
|
|
|
10
11
|
final class UpdateEventEmitter {
|
|
11
12
|
private static WeakReference<ReactApplicationContext> reactContextRef =
|
|
@@ -45,12 +46,20 @@ final class UpdateEventEmitter {
|
|
|
45
46
|
@SuppressWarnings("deprecation")
|
|
46
47
|
private static boolean hasActiveInstance(ReactApplicationContext reactContext) {
|
|
47
48
|
try {
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
// Use reflection for hasActiveReactInstance() because older RN versions (<0.68)
|
|
50
|
+
// don't have this method in the class signature at compile time.
|
|
51
|
+
Method method = reactContext.getClass().getMethod("hasActiveReactInstance");
|
|
52
|
+
Object result = method.invoke(reactContext);
|
|
53
|
+
if (result instanceof Boolean) {
|
|
54
|
+
return (Boolean) result;
|
|
55
|
+
}
|
|
56
|
+
} catch (Throwable ignored) {
|
|
52
57
|
// RN < 0.68 has no hasActiveReactInstance(); fall back for old peers.
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
53
60
|
return reactContext.hasActiveCatalystInstance();
|
|
61
|
+
} catch (Throwable ignored) {
|
|
62
|
+
return false;
|
|
54
63
|
}
|
|
55
64
|
}
|
|
56
65
|
}
|
package/biome.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://biomejs.dev/schemas/2.5.5/schema.json",
|
|
3
|
+
"vcs": {
|
|
4
|
+
"enabled": true,
|
|
5
|
+
"clientKind": "git",
|
|
6
|
+
"useIgnoreFile": true
|
|
7
|
+
},
|
|
8
|
+
"files": {
|
|
9
|
+
"ignoreUnknown": true,
|
|
10
|
+
"includes": [
|
|
11
|
+
"**/*.js",
|
|
12
|
+
"**/*.jsx",
|
|
13
|
+
"**/*.ts",
|
|
14
|
+
"**/*.tsx",
|
|
15
|
+
"**/*.json",
|
|
16
|
+
"!**/node_modules",
|
|
17
|
+
"!**/android",
|
|
18
|
+
"!**/ios",
|
|
19
|
+
"!**/dist",
|
|
20
|
+
"!**/harmony",
|
|
21
|
+
"!**/.tmp"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
"formatter": {
|
|
25
|
+
"enabled": true,
|
|
26
|
+
"indentStyle": "space",
|
|
27
|
+
"indentWidth": 2,
|
|
28
|
+
"lineWidth": 80
|
|
29
|
+
},
|
|
30
|
+
"assist": {
|
|
31
|
+
"actions": {
|
|
32
|
+
"source": {
|
|
33
|
+
"organizeImports": "on"
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"linter": {
|
|
38
|
+
"enabled": true,
|
|
39
|
+
"rules": {
|
|
40
|
+
"preset": "recommended",
|
|
41
|
+
"suspicious": {
|
|
42
|
+
"noExplicitAny": "off"
|
|
43
|
+
},
|
|
44
|
+
"style": {
|
|
45
|
+
"noNonNullAssertion": "off"
|
|
46
|
+
},
|
|
47
|
+
"complexity": {
|
|
48
|
+
"noUselessFragments": "off"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"javascript": {
|
|
53
|
+
"formatter": {
|
|
54
|
+
"quoteStyle": "single",
|
|
55
|
+
"jsxQuoteStyle": "double",
|
|
56
|
+
"trailingCommas": "es5"
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -72,7 +72,7 @@ function toArrayBufferSlice(
|
|
|
72
72
|
offset: number,
|
|
73
73
|
length: number,
|
|
74
74
|
): ArrayBuffer {
|
|
75
|
-
return payload.buffer.slice(
|
|
75
|
+
return (payload.buffer as ArrayBuffer).slice(
|
|
76
76
|
payload.byteOffset + offset,
|
|
77
77
|
payload.byteOffset + offset + length,
|
|
78
78
|
);
|
|
@@ -560,7 +560,7 @@ export class DownloadTask {
|
|
|
560
560
|
'bundle.harmony.js',
|
|
561
561
|
);
|
|
562
562
|
await this.applyBundlePatchFromFileSource(
|
|
563
|
-
originContent,
|
|
563
|
+
originContent.buffer as ArrayBuffer,
|
|
564
564
|
params.unzipDirectory,
|
|
565
565
|
bundlePatchPath,
|
|
566
566
|
`${params.unzipDirectory}/bundle.harmony.js`,
|
package/harmony/pushy.har
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-update",
|
|
3
|
-
"version": "10.48.
|
|
3
|
+
"version": "10.48.3",
|
|
4
4
|
"description": "react-native hot update",
|
|
5
5
|
"main": "src/index",
|
|
6
6
|
"types": "src/index.ts",
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"publish:local": "SKIP_NATIVE_BUILD=1 npm publish",
|
|
10
10
|
"postinstall": "node scripts/check-expo-version.js",
|
|
11
11
|
"prepack": "bun submodule && bun lint",
|
|
12
|
-
"lint": "
|
|
12
|
+
"lint": "biome check src && tsc --noEmit && node scripts/check-harmony-types.js",
|
|
13
13
|
"lint:harmony": "node scripts/check-harmony-types.js",
|
|
14
14
|
"submodule": "git submodule update --init --recursive",
|
|
15
15
|
"test": "bun test src/__tests__",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"build:android-release": "cd Example/e2etest && bun && detox build --configuration android.emu.release",
|
|
26
26
|
"test:android-release": "cd Example/e2etest && E2E_PLATFORM=android bun detox test --configuration android.emu.release --headless --record-logs all",
|
|
27
27
|
"test:android-debug": "cd Example/e2etest && E2E_PLATFORM=android detox test --configuration android.emu.debug --headless --record-logs all",
|
|
28
|
+
"smoke:android-debug": "cd Example/e2etest && detox test --configuration android.emu.debug --config e2e/smoke/jest.config.js",
|
|
28
29
|
"e2e:ios": "bun build:ios-release && bun test:ios-release",
|
|
29
30
|
"e2e:android": "bun build:android-release && bun test:android-release",
|
|
30
31
|
"tests:emulator:prepare": "cd .github/workflows/scripts/functions && bun && bun build",
|
|
@@ -63,8 +64,8 @@
|
|
|
63
64
|
},
|
|
64
65
|
"devDependencies": {
|
|
65
66
|
"@babel/core": "^7.25.8",
|
|
67
|
+
"@biomejs/biome": "^2.5.5",
|
|
66
68
|
"@react-native/babel-preset": "^0.73.21",
|
|
67
|
-
"@react-native/eslint-config": "0.84.1",
|
|
68
69
|
"@react-native/typescript-config": "0.84.1",
|
|
69
70
|
"@types/bun": "^1.3.11",
|
|
70
71
|
"@types/jest": "^30.0.0",
|
|
@@ -72,15 +73,13 @@
|
|
|
72
73
|
"@types/react": "^19.2.14",
|
|
73
74
|
"@types/react-test-renderer": "18.0.7",
|
|
74
75
|
"detox": "^20.50.1",
|
|
75
|
-
"eslint": "^8.57.0",
|
|
76
76
|
"firebase-tools": "^15.11.0",
|
|
77
77
|
"jest": "^30.3.0",
|
|
78
78
|
"pod-install": "^1.0.14",
|
|
79
|
-
"prettier": "^2",
|
|
80
79
|
"react": "18.2.0",
|
|
81
80
|
"react-native": "0.73",
|
|
82
81
|
"react-test-renderer": "18.2.0",
|
|
83
82
|
"ts-jest": "^29.4.6",
|
|
84
|
-
"typescript": "^
|
|
83
|
+
"typescript": "^7.0.2"
|
|
85
84
|
}
|
|
86
85
|
}
|
package/src/NativePushy.ts
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
import {
|
|
2
2
|
DeviceEventEmitter,
|
|
3
|
-
EmitterSubscription,
|
|
3
|
+
type EmitterSubscription,
|
|
4
4
|
Platform,
|
|
5
5
|
} from 'react-native';
|
|
6
6
|
import {
|
|
7
|
-
PushyModule,
|
|
8
7
|
buildTime,
|
|
9
8
|
cInfo,
|
|
10
9
|
currentVersion,
|
|
11
10
|
currentVersionInfo,
|
|
12
|
-
supportedDiffVersion,
|
|
13
11
|
isFirstTime,
|
|
14
12
|
isRolledBack,
|
|
13
|
+
PushyModule,
|
|
15
14
|
packageVersion,
|
|
16
15
|
pushyNativeEventEmitter,
|
|
17
16
|
rolledBackVersion,
|
|
18
17
|
setLocalHashInfo,
|
|
18
|
+
supportedDiffVersion,
|
|
19
19
|
} from './core';
|
|
20
|
+
import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
|
|
21
|
+
import {
|
|
22
|
+
asUpdateErrorCode,
|
|
23
|
+
toUpdateError,
|
|
24
|
+
UpdateError,
|
|
25
|
+
type UpdateErrorCode,
|
|
26
|
+
} from './error';
|
|
27
|
+
import i18n from './i18n';
|
|
20
28
|
import { PermissionsAndroid } from './permissions';
|
|
21
29
|
import {
|
|
30
|
+
resolveServerEventHash,
|
|
31
|
+
resolveServerEventType,
|
|
32
|
+
truncateDetail,
|
|
33
|
+
} from './telemetry';
|
|
34
|
+
import type {
|
|
22
35
|
BeforeReloadContext,
|
|
23
36
|
CheckResult,
|
|
24
37
|
ClientOptions,
|
|
@@ -39,19 +52,6 @@ import {
|
|
|
39
52
|
promiseAny,
|
|
40
53
|
testUrls,
|
|
41
54
|
} from './utils';
|
|
42
|
-
import i18n from './i18n';
|
|
43
|
-
import {
|
|
44
|
-
asUpdateErrorCode,
|
|
45
|
-
toUpdateError,
|
|
46
|
-
UpdateError,
|
|
47
|
-
UpdateErrorCode,
|
|
48
|
-
} from './error';
|
|
49
|
-
import { dedupeEndpoints, executeEndpointFallback } from './endpoint';
|
|
50
|
-
import {
|
|
51
|
-
resolveServerEventHash,
|
|
52
|
-
resolveServerEventType,
|
|
53
|
-
truncateDetail,
|
|
54
|
-
} from './telemetry';
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Receives every error the client reports, alongside the report event type.
|
|
@@ -60,13 +60,16 @@ import {
|
|
|
60
60
|
*/
|
|
61
61
|
export type UpdateErrorListener = (
|
|
62
62
|
error: UpdateError,
|
|
63
|
-
eventType: EventType
|
|
63
|
+
eventType: EventType
|
|
64
64
|
) => void;
|
|
65
65
|
|
|
66
66
|
const SERVER_PRESETS = {
|
|
67
67
|
// cn
|
|
68
68
|
Pushy: {
|
|
69
|
-
main: [
|
|
69
|
+
main: [
|
|
70
|
+
'https://update.react-native.cn/api',
|
|
71
|
+
'https://update.reactnative.cn/api',
|
|
72
|
+
],
|
|
70
73
|
queryUrls: [
|
|
71
74
|
'https://gitee.com/sunnylqm/react-native-pushy/raw/master/endpoints.json',
|
|
72
75
|
'https://cdn.jsdelivr.net/gh/reactnativecn/react-native-update@master/endpoints.json',
|
|
@@ -88,10 +91,10 @@ const cloneServerConfig = (server: UpdateServerConfig): UpdateServerConfig => ({
|
|
|
88
91
|
|
|
89
92
|
const excludeConfiguredEndpoints = (
|
|
90
93
|
endpoints: string[],
|
|
91
|
-
configuredEndpoints: string[]
|
|
94
|
+
configuredEndpoints: string[]
|
|
92
95
|
) => {
|
|
93
96
|
const configured = new Set(configuredEndpoints);
|
|
94
|
-
return endpoints.filter(endpoint => !configured.has(endpoint));
|
|
97
|
+
return endpoints.filter((endpoint) => !configured.has(endpoint));
|
|
95
98
|
};
|
|
96
99
|
|
|
97
100
|
assertWeb();
|
|
@@ -153,7 +156,7 @@ export class Pushy {
|
|
|
153
156
|
version = cInfo.rnu;
|
|
154
157
|
loggerPromise = (() => {
|
|
155
158
|
let resolve: (value?: unknown) => void = () => {};
|
|
156
|
-
const promise = new Promise(res => {
|
|
159
|
+
const promise = new Promise((res) => {
|
|
157
160
|
resolve = res;
|
|
158
161
|
});
|
|
159
162
|
return {
|
|
@@ -167,14 +170,14 @@ export class Pushy {
|
|
|
167
170
|
this.options.server = cloneServerConfig(SERVER_PRESETS[this.clientType]);
|
|
168
171
|
|
|
169
172
|
i18n.setLocale(
|
|
170
|
-
options.locale ?? (this.clientType === 'Pushy' ? 'zh' : 'en')
|
|
173
|
+
options.locale ?? (this.clientType === 'Pushy' ? 'zh' : 'en')
|
|
171
174
|
);
|
|
172
175
|
|
|
173
176
|
if (Platform.OS === 'ios' || Platform.OS === 'android') {
|
|
174
177
|
if (!options.appKey) {
|
|
175
178
|
throw new UpdateError(
|
|
176
179
|
i18n.t('error_appkey_required'),
|
|
177
|
-
'APPKEY_REQUIRED'
|
|
180
|
+
'APPKEY_REQUIRED'
|
|
178
181
|
);
|
|
179
182
|
}
|
|
180
183
|
}
|
|
@@ -235,7 +238,7 @@ export class Pushy {
|
|
|
235
238
|
// no logger is ever provided.
|
|
236
239
|
await Promise.race([
|
|
237
240
|
this.loggerPromise.promise,
|
|
238
|
-
new Promise(resolve => setTimeout(resolve, 10 * 1000)),
|
|
241
|
+
new Promise((resolve) => setTimeout(resolve, 10 * 1000)),
|
|
239
242
|
]);
|
|
240
243
|
}
|
|
241
244
|
const { logger = noop, appKey } = this.options;
|
|
@@ -314,7 +317,7 @@ export class Pushy {
|
|
|
314
317
|
detail: truncateDetail(detail),
|
|
315
318
|
}),
|
|
316
319
|
},
|
|
317
|
-
DEFAULT_FETCH_TIMEOUT_MS
|
|
320
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
318
321
|
).catch((e: any) => {
|
|
319
322
|
log('telemetry report failed:', e?.message || e);
|
|
320
323
|
});
|
|
@@ -366,7 +369,7 @@ export class Pushy {
|
|
|
366
369
|
{
|
|
367
370
|
message = error.message,
|
|
368
371
|
data,
|
|
369
|
-
}: { message?: string; data?: Record<string, string | number> } = {}
|
|
372
|
+
}: { message?: string; data?: Record<string, string | number> } = {}
|
|
370
373
|
) => {
|
|
371
374
|
this.emittedErrors.add(error);
|
|
372
375
|
this.report({
|
|
@@ -424,20 +427,20 @@ export class Pushy {
|
|
|
424
427
|
}
|
|
425
428
|
try {
|
|
426
429
|
const resp = await promiseAny(
|
|
427
|
-
server.queryUrls.map(queryUrl =>
|
|
428
|
-
fetchWithTimeout(queryUrl, {}, DEFAULT_FETCH_TIMEOUT_MS)
|
|
429
|
-
)
|
|
430
|
+
server.queryUrls.map((queryUrl) =>
|
|
431
|
+
fetchWithTimeout(queryUrl, {}, DEFAULT_FETCH_TIMEOUT_MS)
|
|
432
|
+
)
|
|
430
433
|
);
|
|
431
434
|
const remoteEndpoints = await resp.json();
|
|
432
435
|
log('fetch endpoints:', remoteEndpoints);
|
|
433
436
|
if (Array.isArray(remoteEndpoints)) {
|
|
434
437
|
return excludeConfiguredEndpoints(
|
|
435
438
|
dedupeEndpoints(
|
|
436
|
-
|
|
437
|
-
|
|
439
|
+
remoteEndpoints.filter(
|
|
440
|
+
(endpoint): endpoint is string => typeof endpoint === 'string'
|
|
441
|
+
)
|
|
438
442
|
),
|
|
439
|
-
)
|
|
440
|
-
this.getConfiguredCheckEndpoints(),
|
|
443
|
+
this.getConfiguredCheckEndpoints()
|
|
441
444
|
);
|
|
442
445
|
}
|
|
443
446
|
} catch (e) {
|
|
@@ -447,12 +450,12 @@ export class Pushy {
|
|
|
447
450
|
};
|
|
448
451
|
requestCheckResult = async (
|
|
449
452
|
endpoint: string,
|
|
450
|
-
fetchPayload: Parameters<typeof fetch>[1]
|
|
453
|
+
fetchPayload: Parameters<typeof fetch>[1]
|
|
451
454
|
) => {
|
|
452
455
|
const resp = await fetchWithTimeout(
|
|
453
456
|
this.getCheckUrl(endpoint),
|
|
454
457
|
fetchPayload,
|
|
455
|
-
DEFAULT_FETCH_TIMEOUT_MS
|
|
458
|
+
DEFAULT_FETCH_TIMEOUT_MS
|
|
456
459
|
);
|
|
457
460
|
|
|
458
461
|
if (!resp.ok) {
|
|
@@ -463,7 +466,7 @@ export class Pushy {
|
|
|
463
466
|
statusText: respText,
|
|
464
467
|
}),
|
|
465
468
|
'HTTP_STATUS',
|
|
466
|
-
{ extra: { status: resp.status } }
|
|
469
|
+
{ extra: { status: resp.status } }
|
|
467
470
|
);
|
|
468
471
|
}
|
|
469
472
|
|
|
@@ -473,7 +476,7 @@ export class Pushy {
|
|
|
473
476
|
const { endpoint, value } = await executeEndpointFallback<CheckResult>({
|
|
474
477
|
configuredEndpoints: this.getConfiguredCheckEndpoints(),
|
|
475
478
|
getRemoteEndpoints: this.getRemoteEndpoints,
|
|
476
|
-
tryEndpoint: async currentEndpoint => {
|
|
479
|
+
tryEndpoint: async (currentEndpoint) => {
|
|
477
480
|
try {
|
|
478
481
|
return await this.requestCheckResult(currentEndpoint, fetchPayload);
|
|
479
482
|
} catch (e) {
|
|
@@ -666,7 +669,7 @@ export class Pushy {
|
|
|
666
669
|
};
|
|
667
670
|
downloadUpdate = async (
|
|
668
671
|
updateInfo: CheckResult,
|
|
669
|
-
onDownloadProgress?: (data: ProgressData) => void
|
|
672
|
+
onDownloadProgress?: (data: ProgressData) => void
|
|
670
673
|
) => {
|
|
671
674
|
const { hash } = updateInfo;
|
|
672
675
|
if (
|
|
@@ -713,7 +716,7 @@ export class Pushy {
|
|
|
713
716
|
};
|
|
714
717
|
private performDownload = async (
|
|
715
718
|
updateInfo: CheckResult,
|
|
716
|
-
onDownloadProgress?: (data: ProgressData) => void
|
|
719
|
+
onDownloadProgress?: (data: ProgressData) => void
|
|
717
720
|
) => {
|
|
718
721
|
const {
|
|
719
722
|
hash,
|
|
@@ -746,7 +749,9 @@ export class Pushy {
|
|
|
746
749
|
...data,
|
|
747
750
|
progress: computeProgress(data.received, data.total),
|
|
748
751
|
};
|
|
749
|
-
callbacks.forEach(callback =>
|
|
752
|
+
callbacks.forEach((callback) => {
|
|
753
|
+
callback(payload);
|
|
754
|
+
});
|
|
750
755
|
};
|
|
751
756
|
const onNativeProgress = (progressData: ProgressData) => {
|
|
752
757
|
if (progressData.hash === hash) {
|
|
@@ -757,12 +762,12 @@ export class Pushy {
|
|
|
757
762
|
if (Platform.OS === 'harmony') {
|
|
758
763
|
sharedState.progressHandlers[hash] = DeviceEventEmitter.addListener(
|
|
759
764
|
'RCTPushyDownloadProgress',
|
|
760
|
-
onNativeProgress
|
|
765
|
+
onNativeProgress
|
|
761
766
|
);
|
|
762
767
|
} else {
|
|
763
768
|
sharedState.progressHandlers[hash] = pushyNativeEventEmitter.addListener(
|
|
764
769
|
'RCTPushyDownloadProgress',
|
|
765
|
-
onNativeProgress
|
|
770
|
+
onNativeProgress
|
|
766
771
|
);
|
|
767
772
|
}
|
|
768
773
|
const maxRetries = Math.max(0, Math.floor(this.options.maxRetries ?? 3));
|
|
@@ -778,7 +783,10 @@ export class Pushy {
|
|
|
778
783
|
type DownloadStrategy = {
|
|
779
784
|
name: string;
|
|
780
785
|
candidate: string | undefined;
|
|
781
|
-
errorKey:
|
|
786
|
+
errorKey:
|
|
787
|
+
| 'error_diff_failed'
|
|
788
|
+
| 'error_pdiff_failed'
|
|
789
|
+
| 'error_full_patch_failed';
|
|
782
790
|
skipInDev: boolean;
|
|
783
791
|
devNoopWhenNoUrl: boolean;
|
|
784
792
|
run: (url: string) => Promise<void>;
|
|
@@ -790,7 +798,7 @@ export class Pushy {
|
|
|
790
798
|
errorKey: 'error_diff_failed',
|
|
791
799
|
skipInDev: true,
|
|
792
800
|
devNoopWhenNoUrl: false,
|
|
793
|
-
run: url =>
|
|
801
|
+
run: (url) =>
|
|
794
802
|
PushyModule.downloadPatchFromPpk({
|
|
795
803
|
updateUrl: url,
|
|
796
804
|
hash,
|
|
@@ -803,7 +811,7 @@ export class Pushy {
|
|
|
803
811
|
errorKey: 'error_pdiff_failed',
|
|
804
812
|
skipInDev: true,
|
|
805
813
|
devNoopWhenNoUrl: false,
|
|
806
|
-
run: url =>
|
|
814
|
+
run: (url) =>
|
|
807
815
|
PushyModule.downloadPatchFromPackage({
|
|
808
816
|
updateUrl: url,
|
|
809
817
|
hash,
|
|
@@ -815,7 +823,7 @@ export class Pushy {
|
|
|
815
823
|
errorKey: 'error_full_patch_failed',
|
|
816
824
|
skipInDev: false,
|
|
817
825
|
devNoopWhenNoUrl: true,
|
|
818
|
-
run: url =>
|
|
826
|
+
run: (url) =>
|
|
819
827
|
PushyModule.downloadFullUpdate({
|
|
820
828
|
updateUrl: url,
|
|
821
829
|
hash,
|
|
@@ -827,7 +835,7 @@ export class Pushy {
|
|
|
827
835
|
if (attempt > 0) {
|
|
828
836
|
const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000);
|
|
829
837
|
log(`retry attempt ${attempt}/${maxRetries}, waiting ${backoffMs}ms`);
|
|
830
|
-
await new Promise(r => setTimeout(r, backoffMs));
|
|
838
|
+
await new Promise((r) => setTimeout(r, backoffMs));
|
|
831
839
|
errorMessages.length = 0;
|
|
832
840
|
lastError = undefined;
|
|
833
841
|
succeeded = '';
|
|
@@ -860,7 +868,7 @@ export class Pushy {
|
|
|
860
868
|
lastError = new UpdateError(
|
|
861
869
|
errorMessage,
|
|
862
870
|
asUpdateErrorCode(e?.code) ?? 'DOWNLOAD_FAILED',
|
|
863
|
-
{ cause: e }
|
|
871
|
+
{ cause: e }
|
|
864
872
|
);
|
|
865
873
|
log(errorMessage);
|
|
866
874
|
}
|
|
@@ -926,7 +934,7 @@ export class Pushy {
|
|
|
926
934
|
};
|
|
927
935
|
downloadAndInstallApk = async (
|
|
928
936
|
url: string,
|
|
929
|
-
onDownloadProgress?: (data: ProgressData) => void
|
|
937
|
+
onDownloadProgress?: (data: ProgressData) => void
|
|
930
938
|
) => {
|
|
931
939
|
if (Platform.OS !== 'android') {
|
|
932
940
|
return;
|
|
@@ -937,7 +945,7 @@ export class Pushy {
|
|
|
937
945
|
if (sharedState.apkStatus === 'downloaded') {
|
|
938
946
|
const err = new UpdateError(
|
|
939
947
|
this.t('error_apk_pending_install'),
|
|
940
|
-
'APK_INSTALL_PENDING'
|
|
948
|
+
'APK_INSTALL_PENDING'
|
|
941
949
|
);
|
|
942
950
|
this.emitError(err, 'errorInstallApk');
|
|
943
951
|
this.throwIfEnabled(err);
|
|
@@ -946,12 +954,12 @@ export class Pushy {
|
|
|
946
954
|
if (Platform.Version <= 23) {
|
|
947
955
|
try {
|
|
948
956
|
const granted = await PermissionsAndroid.request(
|
|
949
|
-
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
|
|
957
|
+
PermissionsAndroid.PERMISSIONS.WRITE_EXTERNAL_STORAGE
|
|
950
958
|
);
|
|
951
959
|
if (granted !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
952
960
|
const err = new UpdateError(
|
|
953
961
|
this.t('error_storage_permission_rejected'),
|
|
954
|
-
'STORAGE_PERMISSION_REJECTED'
|
|
962
|
+
'STORAGE_PERMISSION_REJECTED'
|
|
955
963
|
);
|
|
956
964
|
this.emitError(err, 'rejectStoragePermission');
|
|
957
965
|
this.throwIfEnabled(err);
|
|
@@ -978,7 +986,7 @@ export class Pushy {
|
|
|
978
986
|
if (progressData.hash === progressKey) {
|
|
979
987
|
onDownloadProgress(progressData);
|
|
980
988
|
}
|
|
981
|
-
}
|
|
989
|
+
}
|
|
982
990
|
);
|
|
983
991
|
}
|
|
984
992
|
try {
|
|
@@ -1045,7 +1053,7 @@ export class Pushy {
|
|
|
1045
1053
|
// native module predates this method.
|
|
1046
1054
|
const err = new UpdateError(
|
|
1047
1055
|
this.t('error_reset_not_supported'),
|
|
1048
|
-
'RESET_FAILED'
|
|
1056
|
+
'RESET_FAILED'
|
|
1049
1057
|
);
|
|
1050
1058
|
this.emitError(err, 'errorReset');
|
|
1051
1059
|
this.throwIfEnabled(err);
|