Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions NATIVE_HOST_API_MIGRATION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Native host API naming migration

The bridge-free host APIs use bundle-preparation terminology. This is a
source-level breaking rename, not a change to download, activation, rollback,
or crash-rescue behavior. No deprecated native alias is retained.

| Previous API | Replacement |
| --- | --- |
| Android `PushyNativeUpdate` | `PushyRuntime` |
| Android `PushyNativeUpdate.checkAndUpdate(context, callback)` | `PushyRuntime.prepareBundle(context, callback)` |
| Objective-C `checkAndUpdateWithCompletion:` | `prepareBundleWithCompletion:` |
| Swift `checkAndUpdate(completion:)` | `prepareBundle(completion:)` |
| Harmony `PushyFileJSBundleProvider.checkAndUpdate()` | `PushyFileJSBundleProvider.prepareBundle()` |
| Android/Harmony `NativeUpdateResult` | `BundlePreparationResult` |
| Harmony `NativeUpdateConfig` | `PushyConfiguration` |
| iOS `RCTPushyNativeUpdateCompletion` | `RCTPushyBundlePreparationCompletion` |
| iOS `RCTPushyNativeConfigurationCompletion` | `RCTPushyConfigurationCompletion` |

`configure` keeps its name. The Android package, iOS module and Harmony package
identities are unchanged. Update native imports, type annotations and call sites
together, including imports from Harmony's package entry point.

## Lifecycle and compatibility

Persist configuration before normal launch bundle resolution when provisioning
without JavaScript. Wait for configuration to finish before continuing startup.
Call `prepareBundle` only **after the host's real launch bundle resolution**, and
reuse the initialized Harmony provider. Do not resolve the bundle a second time
just to call this API: resolution consumes first-load and rollback markers.

The call starts, joins or reuses the process's existing native round. It does not
reload the running React Native instance or display UI. A successful selection is
for the **next launch**; preparation can also download without selecting a bundle,
according to the existing `afterDownload` policy. Callback threading, cancellation,
request deduplication, configuration invalidation and rescue behavior are unchanged.

Result fields (`status`, `reason`, `hash`, `activated`) and existing status strings,
including `noUpdate`, remain unchanged. This avoids changing the data contract as
part of an identifier rename.

This change is limited to the bridge-free host API. The public JavaScript SDK,
TurboModule/legacy bridge method names, server paths such as `/checkUpdate`,
configuration values such as `setNeedUpdate`, and persisted keys are unchanged.
It is not a removal of every occurrence of `update` from the package or binary.

Rebuild and redistribute the native application to consume these native API names;
a JavaScript-only delivery cannot rename an installed native class or selector.
For Apple platforms, rerun `pod install` as part of the normal native upgrade.
For Harmony, rebuild/use the matching HAR instead of an older prebuilt artifact.
Do not restore old native aliases during migration. Historical release notes retain
the API names that existed in the versions they document.

## 中文迁移说明

原生宿主入口统一为 `prepareBundle`,Android 对外类改为 `PushyRuntime`,
结果类型改为 `BundlePreparationResult`,Harmony 配置类型改为
`PushyConfiguration`。iOS/Swift 的方法与完成回调类型(含 `configure` 的
`RCTPushyConfigurationCompletion`)同步改名,不保留旧名别名。

这是原生源码接口的破坏性重命名,不改变下载、下次启动选包、回滚或救援行为。
配置完成后,先走宿主正常的 bundle 解析流程,再调用 `prepareBundle`;
不要为调用接口再次解析 bundle,也不要新建另一个 Harmony provider。
`activated` 仍表示已为下次启动选定,而不是当前实例已经重载。

JS SDK、JS 原生桥接接口、服务端路径、配置值、结果字段/状态值和持久化键不变。
需要重新构建并分发原生安装包,不能只通过 JS 热更新完成原生接口改名;
Apple 平台按正常升级流程重新运行 `pod install`,Harmony 使用重新构建的 HAR。
7 changes: 7 additions & 0 deletions README-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,3 +172,10 @@ Hermes 字节码对通用二进制 diff 极不友好,我们在两个环节同
本组件由[React Native 中文网](https://reactnative.cn/)独家发布,如有定制需求可以[联系我们](https://reactnative.cn/about.html#content)。

关于此插件发现任何问题,可以前往[Issues](https://github.com/reactnativecn/react-native-update/issues)发帖提问。

## 原生宿主接口

原生宿主入口统一为 `prepareBundle`:Android 使用 `PushyRuntime`,
Apple 平台使用 `RCTPushy`,Harmony 使用已有的 `PushyFileJSBundleProvider`。
请在正常启动 bundle 解析后调用。改名不改变 JS 桥接契约,但原生接入代码需要迁移并重新构建。
详见[原生接口迁移说明](NATIVE_HOST_API_MIGRATION.md)。
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,3 +153,11 @@ Since 10.52.1 every cold start runs one background update check that **does not
| **Technical Support** | ✅ Paid dedicated support | ⚠️ Community support | ❌ **Discontinued** |
| **Server Deployment** | ✅ Hosted service or paid private deployment | ✅ Hosted by Expo (EAS Update) | ❌ **Discontinued** |
| **Bandwidth Usage** | ⭐⭐⭐⭐⭐ Very low (incremental) | ⭐⭐⭐ Higher (full bundle) | ❌ **Discontinued** |

## Native host APIs

Bridge-free hosts use `PushyRuntime.prepareBundle` (Android),
`RCTPushy.prepareBundle` (Swift), or `PushyFileJSBundleProvider.prepareBundle`
(HarmonyOS), after normal launch bundle resolution. See the
[native host API migration guide](NATIVE_HOST_API_MIGRATION.md) for the
source-breaking rename, unchanged JS bridge contract, and native rebuild steps.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Snapshot of a native check-and-update round. A downloaded result does not
* mean that the running React Native instance has been reloaded.
*/
public final class NativeUpdateResult {
public final class BundlePreparationResult {
public static final String SKIPPED = "skipped";
public static final String NO_UPDATE = "noUpdate";
public static final String DOWNLOADED = "downloaded";
Expand All @@ -16,19 +16,19 @@ public final class NativeUpdateResult {
private final String hash;
private final boolean activated;

private NativeUpdateResult(String status, String reason, String hash, boolean activated) {
private BundlePreparationResult(String status, String reason, String hash, boolean activated) {
this.status = status;
this.reason = reason;
this.hash = hash;
this.activated = activated;
}

static NativeUpdateResult of(String status, String reason) {
return new NativeUpdateResult(status, reason, "", false);
static BundlePreparationResult of(String status, String reason) {
return new BundlePreparationResult(status, reason, "", false);
}

static NativeUpdateResult downloaded(String hash, boolean activated) {
return new NativeUpdateResult(DOWNLOADED, "", hash, activated);
static BundlePreparationResult downloaded(String hash, boolean activated) {
return new BundlePreparationResult(DOWNLOADED, "", hash, activated);
}

public String getStatus() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,46 +73,46 @@ final class NativeCheckOrchestrator {
private static volatile String sJsCompletedConfig;
// Published after the launch rollback snapshot, before host calls are accepted.
private static volatile boolean nativeReady;
private static volatile NativeUpdateResult roundResult =
NativeUpdateResult.of(NativeUpdateResult.FAILED, "check_failed");
private static volatile BundlePreparationResult roundResult =
BundlePreparationResult.of(BundlePreparationResult.FAILED, "check_failed");
private static volatile long roundGeneration = -1;
private static volatile long roundConfigGeneration = -1;
private static volatile String roundConfigJson;

/** Blocking only on the host API's worker; never call on the UI thread. */
static NativeUpdateResult checkAndUpdate(UpdateContext context) throws InterruptedException {
static BundlePreparationResult prepareBundle(UpdateContext context) throws InterruptedException {
if (UpdateContext.DEBUG) {
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "debug");
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "debug");
}
if (!nativeReady || sContext != context || !context.getIsUsingBundleUrl()) {
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "not_initialized");
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_initialized");
}
String configJson = context.getKv(KEY_CONFIG);
if (configJson == null || configJson.isEmpty()) {
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "not_configured");
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_configured");
}
try {
JSONObject config = new JSONObject(configJson);
if (config.optBoolean("disabled", false)) {
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "disabled");
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled");
}
if (config.optString("appKey", "").isEmpty()) {
return NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
}
} catch (JSONException e) {
return NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
}
startRound(0);
if (!roundStarted.get()) {
return NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "config_changed");
return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "config_changed");
}
roundDone.await();
if (roundConfigGeneration != UpdateContext.getNativeConfigGeneration()
|| !configJson.equals(roundConfigJson) || !configJson.equals(context.getKv(KEY_CONFIG))) {
return NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "config_changed");
return BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "config_changed");
}
if (roundGeneration != UpdateContext.getResetGeneration()) {
return NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "reset");
return BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
}
return roundResult;
}
Expand Down Expand Up @@ -226,7 +226,7 @@ private static void startRound(long deadlineNanos) {
runOnce(sContext, sLaunchRolledBackVersion, deadlineNanos);
} catch (Throwable e) {
Log.w(UpdateContext.TAG, "native check failed: " + e);
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "internal_error");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "internal_error");
} finally {
roundCompleted = true;
roundDone.countDown();
Expand Down Expand Up @@ -303,27 +303,27 @@ private static void runOnce(
final long resetGeneration = UpdateContext.getResetGeneration();
roundGeneration = resetGeneration;
roundConfigGeneration = UpdateContext.getNativeConfigGeneration();
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "check_failed");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "check_failed");
String configJson = context.getKv(KEY_CONFIG);
roundConfigJson = configJson;
if (configJson == null || configJson.isEmpty()) {
roundResult = NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "not_configured");
roundResult = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_configured");
return;
}
JSONObject config;
try {
config = new JSONObject(configJson);
} catch (JSONException e) {
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
return;
}
if (config.optBoolean("disabled", false)) {
roundResult = NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "disabled");
roundResult = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled");
return;
}
String appKey = config.optString("appKey", "");
if (appKey.isEmpty()) {
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_config");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config");
return;
}
// Keep the existing interrupted-round breadcrumb and reset generation.
Expand Down Expand Up @@ -401,7 +401,7 @@ private static void runConfiguredRound(

String body = NativeUpdateFlow.buildCheckRequestBody(input.toString());
if (body == null) {
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_request");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_request");
return;
}

Expand All @@ -418,7 +418,7 @@ private static void runConfiguredRound(
String decisionJson = NativeUpdateFlow.handleCheckResponse(
responseText, identity.toString(), config.optString("afterDownload", ""));
if (decisionJson == null) {
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_response");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_response");
return;
}
JSONObject decision = new JSONObject(decisionJson);
Expand All @@ -427,15 +427,15 @@ private static void runConfiguredRound(
resetGeneration, null, null, false,
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
roundResult = committed
? NativeUpdateResult.of(NativeUpdateResult.NO_UPDATE, decision.optString("reason"))
: NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "reset");
? BundlePreparationResult.of(BundlePreparationResult.NO_UPDATE, decision.optString("reason"))
: BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
Log.i(UpdateContext.TAG,
"native check: nothing to do (" + decision.optString("reason") + ")");
return;
}
String hash = decision.optString("hash", "");
if (!UpdateFileUtils.isSafePathComponent(hash)) {
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "invalid_response");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_response");
return;
}

Expand All @@ -451,8 +451,8 @@ private static void runConfiguredRound(
boolean committed = context.commitNativeCheckResult(
resetGeneration, null, null, false,
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
roundResult = NativeUpdateResult.of(
committed ? NativeUpdateResult.FAILED : NativeUpdateResult.CANCELLED,
roundResult = BundlePreparationResult.of(
committed ? BundlePreparationResult.FAILED : BundlePreparationResult.CANCELLED,
committed ? "download_failed" : "reset");
return;
}
Expand Down Expand Up @@ -499,7 +499,7 @@ private static void runConfiguredRound(
buildResponseCacheJson(configJson, body, responseText, responseAtSeconds));
} catch (Exception e) {
Log.w(UpdateContext.TAG, "native check: commit failed: " + e);
roundResult = NativeUpdateResult.of(NativeUpdateResult.FAILED, "commit_failed");
roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "commit_failed");
return;
}
if (!committed) {
Expand All @@ -517,8 +517,8 @@ private static void runConfiguredRound(
"native check: downloaded " + hash + ", activation left to JS");
}
roundResult = committed
? NativeUpdateResult.downloaded(hash, activate)
: NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "reset");
? BundlePreparationResult.downloaded(hash, activate)
: BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "reset");
}

private static String buildResponseCacheJson(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
import org.json.JSONException;
import org.json.JSONObject;

/** Internal normalizer for the JSONObject accepted by PushyNativeUpdate.configure. */
final class NativeUpdateConfig {
/** Internal normalizer for the JSONObject accepted by PushyRuntime.configure. */
final class PushyConfiguration {
private static final Set<String> KEYS = new HashSet<>(Arrays.asList(
"appKey", "endpoints", "queryUrls", "afterDownload", "disabled",
"packageVersion", "rnu", "rn"));
Expand All @@ -24,7 +24,7 @@ final class NativeUpdateConfig {
"https://cdn.jsdelivr.net/gh/reactnativecn/react-native-update@master/endpoints.json"
};

private NativeUpdateConfig() {}
private PushyConfiguration() {}

static String normalize(String json) throws JSONException {
JSONObject options = new JSONObject(json);
Expand Down
Loading
Loading