From afbd849583d9e578e3bfa51b09eaaf570a6657a5 Mon Sep 17 00:00:00 2001 From: Sunny Luo Date: Mon, 28 Sep 2026 16:35:56 +0800 Subject: [PATCH 1/6] test: define native host bundle-preparation naming contract --- src/__tests__/nativeHostApiNaming.test.ts | 64 +++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 src/__tests__/nativeHostApiNaming.test.ts diff --git a/src/__tests__/nativeHostApiNaming.test.ts b/src/__tests__/nativeHostApiNaming.test.ts new file mode 100644 index 00000000..3b8fdce9 --- /dev/null +++ b/src/__tests__/nativeHostApiNaming.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, test } from 'bun:test'; +import { existsSync, readFileSync } from 'node:fs'; + +const root = new URL('../../', import.meta.url); +const android = 'android/src/main/java/cn/reactnative/modules/update/'; +const harmony = 'harmony/pushy/src/main/ets/'; +const source = (path: string) => readFileSync(new URL(path, root), 'utf8'); + +// These are forbidden host API identifiers, not backend paths or JS bridge names. +const oldNames = /\b(?:PushyNativeUpdate|NativeUpdateResult|NativeUpdateConfig|RCTPushyNativeUpdateCompletion|checkAndUpdate(?:WithCompletion|Native)?)\b/; + +describe('native host API naming', () => { + test('Android exposes prepareBundle through PushyRuntime', () => { + const entry = source(`${android}PushyRuntime.java`); + expect(entry).toContain('public final class PushyRuntime'); + expect(entry).toContain('public static void prepareBundle('); + expect(entry).toContain('NativeCheckOrchestrator.prepareBundle('); + expect(entry).toContain('void onComplete(BundlePreparationResult result)'); + expect(source(`${android}BundlePreparationResult.java`)).toContain('class BundlePreparationResult'); + expect(source(`${android}PushyConfiguration.java`)).toContain('class PushyConfiguration'); + for (const name of ['PushyNativeUpdate', 'NativeUpdateResult', 'NativeUpdateConfig']) { + expect(existsSync(new URL(`${android}${name}.java`, root))).toBe(false); + } + }); + + test('Objective-C declarations, implementation and Swift selector agree', () => { + const header = source('ios/RCTPushy/RCTPushy.h'); + const implementation = source('ios/RCTPushy/RCTPushy.mm'); + expect(header).toContain('prepareBundleWithCompletion:'); + expect(header).toContain('RCTPushyBundlePreparationCompletion'); + expect(header).toContain('NS_SWIFT_NAME(prepareBundle(completion:))'); + expect(implementation).toContain('prepareBundleWithCompletion:'); + expect(implementation).toContain('[RCTPushyOrchestrator prepareBundle]'); + expect(header).not.toMatch(oldNames); + expect(implementation).not.toMatch(oldNames); + }); + + test('Harmony exports the renamed result and configuration types', () => { + const provider = source(`${harmony}PushyFileJSBundleProvider.ets`); + const exports = source('harmony/pushy/index.ets'); + expect(provider).toContain('prepareBundle(): Promise'); + expect(provider).toContain('return prepareBundleNative(this.updateContext)'); + expect(provider).toContain('configure(options: PushyConfiguration)'); + expect(exports).toContain('export type { BundlePreparationResult }'); + expect(exports).toContain('export type { PushyConfiguration }'); + expect(provider).not.toMatch(oldNames); + expect(exports).not.toMatch(oldNames); + for (const name of ['NativeUpdateResult', 'NativeUpdateConfig']) { + expect(existsSync(new URL(`${harmony}${name}.ts`, root))).toBe(false); + } + }); + + test('renamed host entry files contain no deprecated native aliases', () => { + for (const path of [ + `${android}PushyRuntime.java`, + `${android}BundlePreparationResult.java`, + `${android}PushyConfiguration.java`, + `${harmony}BundlePreparationResult.ts`, + `${harmony}PushyConfiguration.ts`, + ]) { + expect(source(path)).not.toMatch(oldNames); + } + }); +}); From 4cc1bbc76df74a0d4ff7cf5d4672da03a452eb94 Mon Sep 17 00:00:00 2001 From: Sunny Luo Date: Mon, 28 Sep 2026 16:36:34 +0800 Subject: [PATCH 2/6] docs: document native host API naming migration and compatibility boundary --- NATIVE_HOST_API_MIGRATION.md | 65 ++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 NATIVE_HOST_API_MIGRATION.md diff --git a/NATIVE_HOST_API_MIGRATION.md b/NATIVE_HOST_API_MIGRATION.md new file mode 100644 index 00000000..eabe4835 --- /dev/null +++ b/NATIVE_HOST_API_MIGRATION.md @@ -0,0 +1,65 @@ +# 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` | + +`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 的方法与完成回调类型同步改名,不保留旧名别名。 + +这是原生源码接口的破坏性重命名,不改变下载、下次启动选包、回滚或救援行为。 +配置完成后,先走宿主正常的 bundle 解析流程,再调用 `prepareBundle`; +不要为调用接口再次解析 bundle,也不要新建另一个 Harmony provider。 +`activated` 仍表示已为下次启动选定,而不是当前实例已经重载。 + +JS SDK、JS 原生桥接接口、服务端路径、配置值、结果字段/状态值和持久化键不变。 +需要重新构建并分发原生安装包,不能只通过 JS 热更新完成原生接口改名; +Apple 平台按正常升级流程重新运行 `pod install`,Harmony 使用重新构建的 HAR。 From a30392c7191c41344455f095cba062535c4fe7fa Mon Sep 17 00:00:00 2001 From: Sunny Luo Date: Mon, 28 Sep 2026 16:38:05 +0800 Subject: [PATCH 3/6] chore: prepare branch-scoped native host API refactor --- .../workflows/native-host-api-refactor.yml | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 .github/workflows/native-host-api-refactor.yml diff --git a/.github/workflows/native-host-api-refactor.yml b/.github/workflows/native-host-api-refactor.yml new file mode 100644 index 00000000..565b74bc --- /dev/null +++ b/.github/workflows/native-host-api-refactor.yml @@ -0,0 +1,125 @@ +name: Prepare native host API refactor + +on: + push: + branches: + - refactor/native-host-prepare-bundle + paths: + - .github/workflows/native-host-api-refactor.yml + +permissions: + contents: write + +concurrency: + group: native-host-api-refactor-${{ github.ref }} + cancel-in-progress: false + +jobs: + refactor: + if: github.repository == 'reactnativecn/react-native-update' && github.ref == 'refs/heads/refactor/native-host-prepare-bundle' + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v7 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: 1.4.0 + + - name: Rename native host identifiers and matching source files + id: rename + shell: bash + run: | + python3 - <<'PY' + from collections import Counter + from pathlib import Path + import re + import subprocess + + names = { + 'checkAndUpdateWithCompletion': 'prepareBundleWithCompletion', + 'checkAndUpdateNative': 'prepareBundleNative', + 'checkAndUpdate': 'prepareBundle', + 'RCTPushyNativeUpdateCompletion': 'RCTPushyBundlePreparationCompletion', + 'PushyNativeUpdate': 'PushyRuntime', + 'NativeUpdateResult': 'BundlePreparationResult', + 'nativeUpdateResult': 'bundlePreparationResult', + 'NativeUpdateConfig': 'PushyConfiguration', + 'normalizeNativeUpdateConfig': 'normalizePushyConfiguration', + } + pattern = re.compile(r'\b(?:' + '|'.join(map(re.escape, sorted(names, key=len, reverse=True))) + r')\b') + excluded = { + 'NATIVE_HOST_API_MIGRATION.md', + 'src/__tests__/nativeHostApiNaming.test.ts', + } + android = Path('android/src/main/java/cn/reactnative/modules/update') + assert (android / 'PushyNativeUpdate.java').is_file(), 'Expected the original host API' + bridge = Path('src/NativePushy.ts').read_bytes() + package = Path('package.json').read_bytes() + lock = Path('bun.lock').read_bytes() + counts = Counter() + tracked = subprocess.check_output(['git', 'ls-files', '-z']).decode().split('\0') + for relative in tracked: + if not relative or relative in excluded or relative.startswith('.github/'): + continue + path = Path(relative) + if not path.is_file() or path.is_symlink(): + continue + original = path.read_bytes() + if b'\0' in original: + continue + try: + text = original.decode('utf-8') + except UnicodeDecodeError: + continue + def replace(match): + counts[match.group()] += 1 + return names[match.group()] + replaced = pattern.sub(replace, text) + if replaced != text: + path.write_bytes(replaced.encode('utf-8')) + print('edited', relative) + if path.stem in names: + destination = path.with_name(names[path.stem] + path.suffix) + assert not destination.exists(), f'Refusing to overwrite {destination}' + path.rename(destination) + print('renamed', relative, '->', destination) + assert all(counts[name] > 0 for name in names), counts + assert Path('src/NativePushy.ts').read_bytes() == bridge, 'JS bridge ABI must remain unchanged' + assert Path('package.json').read_bytes() == package, 'Do not change package identity/version' + assert Path('bun.lock').read_bytes() == lock, 'Do not change dependencies' + for filename, section in { + 'README.md': '\n## Native host APIs\n\nBridge-free hosts use `PushyRuntime.prepareBundle` (Android),\n`RCTPushy.prepareBundle` (Swift), or `PushyFileJSBundleProvider.prepareBundle`\n(HarmonyOS), after normal launch bundle resolution. See the\n[native host API migration guide](NATIVE_HOST_API_MIGRATION.md) for the\nsource-breaking rename, unchanged JS bridge contract, and native rebuild steps.\n', + 'README-CN.md': '\n## 原生宿主接口\n\n原生宿主入口统一为 `prepareBundle`:Android 使用 `PushyRuntime`,\nApple 平台使用 `RCTPushy`,Harmony 使用已有的 `PushyFileJSBundleProvider`。\n请在正常启动 bundle 解析后调用。改名不改变 JS 桥接契约,但原生接入代码需要迁移并重新构建。\n详见[原生接口迁移说明](NATIVE_HOST_API_MIGRATION.md)。\n', + }.items(): + with Path(filename).open('a', encoding='utf-8') as stream: + stream.write(section) + print('Replacement counts:', dict(counts)) + PY + + - name: Install locked dependencies + run: bun install --frozen-lockfile + + - name: Format changed JavaScript tests + shell: bash + run: | + mapfile -t changed < <(git diff --name-only --diff-filter=ACM -- src | grep -E '\.(ts|tsx)$' || true) + ./node_modules/.bin/biome check --write src/__tests__/nativeHostApiNaming.test.ts "${changed[@]}" + + - name: Run unit tests + run: bun test src/__tests__ + + - name: Run lint, TypeScript and native parity checks + if: success() || failure() + run: bun run lint + + - name: Commit source changes to this feature branch only + if: always() && steps.rename.outcome == 'success' + shell: bash + run: | + test "${GITHUB_REF}" = 'refs/heads/refactor/native-host-prepare-bundle' + git diff --exit-code -- package.json bun.lock src/NativePushy.ts .github + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add -A + git commit -m 'refactor!: rename bridge-free native host APIs to bundle preparation' + git push origin HEAD:refs/heads/refactor/native-host-prepare-bundle From cad5d520c6fd7d67c766e8ebb26b8c3e6fc1c91d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 28 Sep 2026 08:38:18 +0000 Subject: [PATCH 4/6] refactor!: rename bridge-free native host APIs to bundle preparation --- README-CN.md | 7 ++ README.md | 8 ++ ...sult.java => BundlePreparationResult.java} | 12 +-- .../update/NativeCheckOrchestrator.java | 56 +++++++------- ...ateConfig.java => PushyConfiguration.java} | 6 +- ...shyNativeUpdate.java => PushyRuntime.java} | 24 +++--- harmony/pushy/index.ets | 4 +- ...teResult.ts => BundlePreparationResult.ts} | 10 +-- .../src/main/ets/NativeCheckOrchestrator.ts | 76 +++++++++---------- ...eUpdateConfig.ts => PushyConfiguration.ts} | 4 +- .../main/ets/PushyFileJSBundleProvider.ets | 16 ++-- ios/RCTPushy/RCTPushy.h | 6 +- ios/RCTPushy/RCTPushy.mm | 8 +- src/__tests__/nativeConfiguration.test.ts | 14 ++-- src/__tests__/nativeHostApi.test.ts | 28 ++++--- src/__tests__/nativeHostApiNaming.test.ts | 25 ++++-- src/__tests__/nativeUpdateRound.test.ts | 26 +++---- 17 files changed, 181 insertions(+), 149 deletions(-) rename android/src/main/java/cn/reactnative/modules/update/{NativeUpdateResult.java => BundlePreparationResult.java} (72%) rename android/src/main/java/cn/reactnative/modules/update/{NativeUpdateConfig.java => PushyConfiguration.java} (96%) rename android/src/main/java/cn/reactnative/modules/update/{PushyNativeUpdate.java => PushyRuntime.java} (84%) rename harmony/pushy/src/main/ets/{NativeUpdateResult.ts => BundlePreparationResult.ts} (74%) rename harmony/pushy/src/main/ets/{NativeUpdateConfig.ts => PushyConfiguration.ts} (97%) diff --git a/README-CN.md b/README-CN.md index 738c1746..610a3133 100644 --- a/README-CN.md +++ b/README-CN.md @@ -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)。 diff --git a/README.md b/README.md index b9e9a574..a9d7cb2e 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/android/src/main/java/cn/reactnative/modules/update/NativeUpdateResult.java b/android/src/main/java/cn/reactnative/modules/update/BundlePreparationResult.java similarity index 72% rename from android/src/main/java/cn/reactnative/modules/update/NativeUpdateResult.java rename to android/src/main/java/cn/reactnative/modules/update/BundlePreparationResult.java index 43a4452b..e472a836 100644 --- a/android/src/main/java/cn/reactnative/modules/update/NativeUpdateResult.java +++ b/android/src/main/java/cn/reactnative/modules/update/BundlePreparationResult.java @@ -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"; @@ -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() { diff --git a/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java b/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java index b09ee496..d5ee66a0 100644 --- a/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java +++ b/android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java @@ -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; } @@ -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(); @@ -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. @@ -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; } @@ -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); @@ -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; } @@ -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; } @@ -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) { @@ -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( diff --git a/android/src/main/java/cn/reactnative/modules/update/NativeUpdateConfig.java b/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java similarity index 96% rename from android/src/main/java/cn/reactnative/modules/update/NativeUpdateConfig.java rename to android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java index d7561783..e0abb27b 100644 --- a/android/src/main/java/cn/reactnative/modules/update/NativeUpdateConfig.java +++ b/android/src/main/java/cn/reactnative/modules/update/PushyConfiguration.java @@ -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 KEYS = new HashSet<>(Arrays.asList( "appKey", "endpoints", "queryUrls", "afterDownload", "disabled", "packageVersion", "rnu", "rn")); @@ -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); diff --git a/android/src/main/java/cn/reactnative/modules/update/PushyNativeUpdate.java b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java similarity index 84% rename from android/src/main/java/cn/reactnative/modules/update/PushyNativeUpdate.java rename to android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java index c523443a..9a14e059 100644 --- a/android/src/main/java/cn/reactnative/modules/update/PushyNativeUpdate.java +++ b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java @@ -11,10 +11,10 @@ import java.util.concurrent.ThreadFactory; /** Bridge-free native configuration and update APIs. */ -public final class PushyNativeUpdate { +public final class PushyRuntime { public interface Callback { /** Always called on the main thread, including skipped and failed checks. */ - void onComplete(NativeUpdateResult result); + void onComplete(BundlePreparationResult result); } // A single waiting worker, not one thread per caller. The actual round is @@ -46,7 +46,7 @@ public Thread newThread(Runnable runnable) { /** * Validate and persist a complete configuration, even before JS or bundle * resolution. This starts no network work and never resolves a bundle. - * Await the callback before continuing startup/checkAndUpdate. Unless JS + * Await the callback before continuing startup/prepareBundle. Unless JS * uses nativeConfigSource: 'native', later JS config writes can replace it. */ public static void configure(Context context, JSONObject options, final ConfigurationCallback callback) { @@ -61,7 +61,7 @@ public static void configure(Context context, JSONObject options, final Configur public void run() { Exception failure = null; try { - String config = NativeUpdateConfig.normalize(snapshot); + String config = PushyConfiguration.normalize(snapshot); UpdateContext.getInstance(applicationContext).setNativeConfig(config); } catch (Exception e) { failure = e; @@ -79,7 +79,7 @@ public void run() { }); } - private PushyNativeUpdate() { + private PushyRuntime() { } /** @@ -91,7 +91,7 @@ private PushyNativeUpdate() { * this method: bundle resolution consumes first-load/rollback markers. * Missing persisted configuration and debug builds are reported as skipped. */ - public static void checkAndUpdate(Context context, final Callback callback) { + public static void prepareBundle(Context context, final Callback callback) { if (context == null || callback == null) { throw new IllegalArgumentException("context and callback are required"); } @@ -99,22 +99,22 @@ public static void checkAndUpdate(Context context, final Callback callback) { WORKER.execute(new Runnable() { @Override public void run() { - NativeUpdateResult outcome; + BundlePreparationResult outcome; try { if (BuildConfig.DEBUG) { - outcome = NativeUpdateResult.of(NativeUpdateResult.SKIPPED, "debug"); + outcome = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "debug"); } else { - outcome = NativeCheckOrchestrator.checkAndUpdate( + outcome = NativeCheckOrchestrator.prepareBundle( UpdateContext.getInstance(applicationContext)); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); - outcome = NativeUpdateResult.of(NativeUpdateResult.CANCELLED, "interrupted"); + outcome = BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "interrupted"); } catch (Exception | LinkageError e) { Log.w("react-native-update", "native host check failed", e); - outcome = NativeUpdateResult.of(NativeUpdateResult.FAILED, "internal_error"); + outcome = BundlePreparationResult.of(BundlePreparationResult.FAILED, "internal_error"); } - final NativeUpdateResult result = outcome; + final BundlePreparationResult result = outcome; new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { diff --git a/harmony/pushy/index.ets b/harmony/pushy/index.ets index b5bfdbae..3282254f 100644 --- a/harmony/pushy/index.ets +++ b/harmony/pushy/index.ets @@ -2,5 +2,5 @@ export { PushyPackage as default } from './src/main/ets/PushyPackage'; export { PushyPackage } from './src/main/ets/PushyPackage'; export { PushyTurboModule } from './src/main/ets/PushyTurboModule'; export { PushyFileJSBundleProvider } from './src/main/ets/PushyFileJSBundleProvider'; -export type { NativeUpdateResult } from './src/main/ets/NativeUpdateResult'; -export type { NativeUpdateConfig } from './src/main/ets/NativeUpdateConfig'; +export type { BundlePreparationResult } from './src/main/ets/BundlePreparationResult'; +export type { PushyConfiguration } from './src/main/ets/PushyConfiguration'; diff --git a/harmony/pushy/src/main/ets/NativeUpdateResult.ts b/harmony/pushy/src/main/ets/BundlePreparationResult.ts similarity index 74% rename from harmony/pushy/src/main/ets/NativeUpdateResult.ts rename to harmony/pushy/src/main/ets/BundlePreparationResult.ts index 5489091e..e50eee04 100644 --- a/harmony/pushy/src/main/ets/NativeUpdateResult.ts +++ b/harmony/pushy/src/main/ets/BundlePreparationResult.ts @@ -1,5 +1,5 @@ /** A native round snapshot, not the current state of the running RN instance. */ -export interface NativeUpdateResult { +export interface BundlePreparationResult { /** skipped, noUpdate, downloaded, failed, or cancelled. */ status: string; reason: string; @@ -8,20 +8,20 @@ export interface NativeUpdateResult { activated: boolean; } -export function nativeUpdateResult( +export function bundlePreparationResult( status: string, reason: string = '', hash: string = '', activated: boolean = false, -): NativeUpdateResult { +): BundlePreparationResult { return { status, reason, hash, activated }; } /** Internal gate shared by the delayed check and the native host API. */ export class NativeUpdateRound { - private task: Promise | undefined; + private task: Promise | undefined; - run(operation: () => Promise): Promise { + run(operation: () => Promise): Promise { if (this.task !== undefined) { return this.task; } diff --git a/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts b/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts index 64543a37..44b7b775 100644 --- a/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +++ b/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts @@ -3,8 +3,8 @@ import deviceInfo from '@ohos.deviceInfo'; import logger from './Logger'; import NativePatchCore from './NativePatchCore'; import type { UpdateContext } from './UpdateContext'; -import { NativeUpdateRound, nativeUpdateResult } from './NativeUpdateResult'; -import type { NativeUpdateResult } from './NativeUpdateResult'; +import { NativeUpdateRound, bundlePreparationResult } from './BundlePreparationResult'; +import type { BundlePreparationResult } from './BundlePreparationResult'; import { isSafePathComponent } from './PathUtils'; import { monotonicNowMs } from './MonotonicClock'; import { @@ -115,12 +115,12 @@ let scheduledRollback = ''; let roundGeneration = -1; let roundConfigGeneration = -1; let roundConfigJson: string | undefined; -let roundResult = nativeUpdateResult('failed', 'check_failed'); +let roundResult = bundlePreparationResult('failed', 'check_failed'); function startNativeRound( context: UpdateContext, launchRolledBackVersion: string, -): Promise { +): Promise { const preflight = configurationError(context); if (preflight !== undefined) { return Promise.resolve(preflight); @@ -130,7 +130,7 @@ function startNativeRound( await runOnce(context, launchRolledBackVersion); } catch (e) { logger.error(TAG, `native check failed: ${getErrorMessage(e)}`); - roundResult = nativeUpdateResult('failed', 'internal_error'); + roundResult = bundlePreparationResult('failed', 'internal_error'); } return roundResult; }); @@ -138,62 +138,62 @@ function startNativeRound( // A preflight skip must not consume the process's only round: configuration // may arrive after the delayed startup timer on a first-ever launch. -function configurationError(context: UpdateContext): NativeUpdateResult | undefined { +function configurationError(context: UpdateContext): BundlePreparationResult | undefined { const json = context.getKv(KEY_CONFIG); if (!json) { - return nativeUpdateResult('skipped', 'not_configured'); + return bundlePreparationResult('skipped', 'not_configured'); } try { const config = JSON.parse(json) as NativeConfig; if (!config || typeof config !== 'object' || Array.isArray(config)) { - return nativeUpdateResult('failed', 'invalid_config'); + return bundlePreparationResult('failed', 'invalid_config'); } if (config.disabled) { - return nativeUpdateResult('skipped', 'disabled'); + return bundlePreparationResult('skipped', 'disabled'); } if (typeof config.appKey !== 'string' || config.appKey.trim().length === 0) { - return nativeUpdateResult('failed', 'invalid_config'); + return bundlePreparationResult('failed', 'invalid_config'); } } catch (e) { - return nativeUpdateResult('failed', 'invalid_config'); + return bundlePreparationResult('failed', 'invalid_config'); } return undefined; } -export async function checkAndUpdateNative( +export async function prepareBundleNative( context: UpdateContext, -): Promise { +): Promise { if (scheduledContext !== context) { - return nativeUpdateResult('skipped', 'not_initialized'); + return bundlePreparationResult('skipped', 'not_initialized'); } const configJson = context.getKv(KEY_CONFIG); if (!configJson) { - return nativeUpdateResult('skipped', 'not_configured'); + return bundlePreparationResult('skipped', 'not_configured'); } try { const config = JSON.parse(configJson) as NativeConfig; if (!config || typeof config !== 'object' || Array.isArray(config)) { - return nativeUpdateResult('failed', 'invalid_config'); + return bundlePreparationResult('failed', 'invalid_config'); } if (config.disabled) { - return nativeUpdateResult('skipped', 'disabled'); + return bundlePreparationResult('skipped', 'disabled'); } if (typeof config.appKey !== 'string' || !config.appKey) { - return nativeUpdateResult('failed', 'invalid_config'); + return bundlePreparationResult('failed', 'invalid_config'); } } catch (e) { - return nativeUpdateResult('failed', 'invalid_config'); + return bundlePreparationResult('failed', 'invalid_config'); } const result = await startNativeRound(context, scheduledRollback); if (roundConfigGeneration !== context.getNativeConfigGeneration() || configJson !== roundConfigJson || configJson !== context.getKv(KEY_CONFIG)) { - return nativeUpdateResult('cancelled', 'config_changed'); + return bundlePreparationResult('cancelled', 'config_changed'); } if (roundGeneration !== context.getResetGeneration()) { - return nativeUpdateResult('cancelled', 'reset'); + return bundlePreparationResult('cancelled', 'reset'); } // Do not let a caller mutate the cached result observed by later callers. - return nativeUpdateResult(result.status, result.reason, result.hash, result.activated); + return bundlePreparationResult(result.status, result.reason, result.hash, result.activated); } // JS 在本进程内已拿到有效检查响应时对应的配置 JSON(markJsCheckCompleted)。 @@ -250,32 +250,32 @@ async function runOnce( const resetGeneration = context.getResetGeneration(); roundGeneration = resetGeneration; roundConfigGeneration = context.getNativeConfigGeneration(); - roundResult = nativeUpdateResult('failed', 'check_failed'); + roundResult = bundlePreparationResult('failed', 'check_failed'); const configJson = context.getKv(KEY_CONFIG); roundConfigJson = configJson; if (!configJson) { // No persisted configuration: report the rollout gate to native callers. - roundResult = nativeUpdateResult('skipped', 'not_configured'); + roundResult = bundlePreparationResult('skipped', 'not_configured'); return; } let config: NativeConfig; try { config = JSON.parse(configJson) as NativeConfig; if (!config || typeof config !== 'object' || Array.isArray(config)) { - roundResult = nativeUpdateResult('failed', 'invalid_config'); + roundResult = bundlePreparationResult('failed', 'invalid_config'); return; } } catch (e) { - roundResult = nativeUpdateResult('failed', 'invalid_config'); + roundResult = bundlePreparationResult('failed', 'invalid_config'); return; } if (config.disabled) { - roundResult = nativeUpdateResult('skipped', 'disabled'); + roundResult = bundlePreparationResult('skipped', 'disabled'); return; } const appKey = config.appKey ?? ''; if (!appKey) { - roundResult = nativeUpdateResult('failed', 'invalid_config'); + roundResult = bundlePreparationResult('failed', 'invalid_config'); return; } // 从这里起本轮开始做真实工作:留下面包屑,死于轮中时下次启动零延迟续传。 @@ -371,7 +371,7 @@ async function runConfiguredRound( }; const body = NativePatchCore.buildCheckRequestBody(JSON.stringify(input)); if (!body) { - roundResult = nativeUpdateResult('failed', 'invalid_request'); + roundResult = bundlePreparationResult('failed', 'invalid_request'); return; } @@ -390,7 +390,7 @@ async function runConfiguredRound( config.afterDownload ?? '', ); if (!decisionJson) { - roundResult = nativeUpdateResult('failed', 'invalid_response'); + roundResult = bundlePreparationResult('failed', 'invalid_response'); return; } const decision = JSON.parse(decisionJson) as Decision; @@ -403,15 +403,15 @@ async function runConfiguredRound( buildResponseCacheJson(configJson, body, responseText, responseAtSeconds), ); roundResult = committed - ? nativeUpdateResult('noUpdate', decision.reason ?? '') - : nativeUpdateResult('cancelled', 'reset'); + ? bundlePreparationResult('noUpdate', decision.reason ?? '') + : bundlePreparationResult('cancelled', 'reset'); logger.info(TAG, `nothing to do (${decision.reason ?? ''})`); return; } const hash = decision.hash ?? ''; if (!isSafePathComponent(hash)) { logger.warn(TAG, 'decision carries an unsafe hash, ignoring'); - roundResult = nativeUpdateResult('failed', 'invalid_response'); + roundResult = bundlePreparationResult('failed', 'invalid_response'); return; } @@ -438,8 +438,8 @@ async function runConfiguredRound( buildResponseCacheJson(configJson, body, responseText, responseAtSeconds), ); roundResult = committed - ? nativeUpdateResult('failed', 'download_failed') - : nativeUpdateResult('cancelled', 'reset'); + ? bundlePreparationResult('failed', 'download_failed') + : bundlePreparationResult('cancelled', 'reset'); return; } @@ -481,7 +481,7 @@ async function runConfiguredRound( ); } catch (e) { logger.error(TAG, `commit failed: ${getErrorMessage(e)}`); - roundResult = nativeUpdateResult('failed', 'commit_failed'); + roundResult = bundlePreparationResult('failed', 'commit_failed'); return; } if (!committed) { @@ -492,8 +492,8 @@ async function runConfiguredRound( logger.info(TAG, `downloaded ${hash}, activation left to JS`); } roundResult = committed - ? nativeUpdateResult('downloaded', '', hash, activate) - : nativeUpdateResult('cancelled', 'reset'); + ? bundlePreparationResult('downloaded', '', hash, activate) + : bundlePreparationResult('cancelled', 'reset'); } function buildResponseCacheJson( diff --git a/harmony/pushy/src/main/ets/NativeUpdateConfig.ts b/harmony/pushy/src/main/ets/PushyConfiguration.ts similarity index 97% rename from harmony/pushy/src/main/ets/NativeUpdateConfig.ts rename to harmony/pushy/src/main/ets/PushyConfiguration.ts index ccaff6a2..76c0b64c 100644 --- a/harmony/pushy/src/main/ets/NativeUpdateConfig.ts +++ b/harmony/pushy/src/main/ets/PushyConfiguration.ts @@ -1,5 +1,5 @@ /** Options accepted by PushyFileJSBundleProvider.configure(). */ -export interface NativeUpdateConfig { +export interface PushyConfiguration { appKey: string; /** Omitted: Pushy's built-in endpoints. Custom endpoints never inherit discovery URLs. */ endpoints?: string[]; @@ -67,7 +67,7 @@ function configUrls(values: string[], name: string, base: boolean): string[] { } /** Pure validation, before any native state is touched. Does not mutate options. */ -export function normalizeNativeUpdateConfig(options: NativeUpdateConfig): string { +export function normalizePushyConfiguration(options: PushyConfiguration): string { if (!options || typeof options !== 'object' || Array.isArray(options)) { throw new Error('Invalid native configuration: expected an object'); } diff --git a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets index 8976d847..928ab05f 100644 --- a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +++ b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets @@ -6,10 +6,10 @@ import { import common from '@ohos.app.ability.common'; import fs from '@ohos.file.fs'; import { UpdateContext } from './UpdateContext'; -import { checkAndUpdateNative } from './NativeCheckOrchestrator'; -import type { NativeUpdateResult } from './NativeUpdateResult'; -import { normalizeNativeUpdateConfig } from './NativeUpdateConfig'; -import type { NativeUpdateConfig } from './NativeUpdateConfig'; +import { prepareBundleNative } from './NativeCheckOrchestrator'; +import type { BundlePreparationResult } from './BundlePreparationResult'; +import { normalizePushyConfiguration } from './PushyConfiguration'; +import type { PushyConfiguration } from './PushyConfiguration'; export class PushyFileJSBundleProvider extends JSBundleProvider { private updateContext: UpdateContext; @@ -48,14 +48,14 @@ export class PushyFileJSBundleProvider extends JSBundleProvider { } /** Configure before normal bundle resolution; no JS or network work is required. */ - async configure(options: NativeUpdateConfig): Promise { - const config = normalizeNativeUpdateConfig(options); + async configure(options: PushyConfiguration): Promise { + const config = normalizePushyConfiguration(options); await this.updateContext.setNativeConfig(config); } /** Call after the host's real bundle resolution; never resolves it again. */ - checkAndUpdate(): Promise { - return checkAndUpdateNative(this.updateContext); + prepareBundle(): Promise { + return prepareBundleNative(this.updateContext); } getAppKeys(): string[] { diff --git a/ios/RCTPushy/RCTPushy.h b/ios/RCTPushy/RCTPushy.h index a2e5b44e..e41a2945 100644 --- a/ios/RCTPushy/RCTPushy.h +++ b/ios/RCTPushy/RCTPushy.h @@ -3,7 +3,7 @@ typedef void (^RCTPushyNativeConfigurationCompletion)(NSError * _Nullable error); -typedef void (^RCTPushyNativeUpdateCompletion)(NSDictionary * _Nonnull result); +typedef void (^RCTPushyBundlePreparationCompletion)(NSDictionary * _Nonnull result); @interface RCTPushy : RCTEventEmitter @@ -27,7 +27,7 @@ typedef void (^RCTPushyNativeUpdateCompletion)(NSDictionary * _N * hash (empty when nothing was installed), activated (selected for NEXT * launch, not a reload of the running instance). */ -+ (void)checkAndUpdateWithCompletion:(RCTPushyNativeUpdateCompletion _Nullable)completion - NS_SWIFT_NAME(checkAndUpdate(completion:)); ++ (void)prepareBundleWithCompletion:(RCTPushyBundlePreparationCompletion _Nullable)completion + NS_SWIFT_NAME(prepareBundle(completion:)); @end diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index c4396982..1327c73f 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -738,7 +738,7 @@ + (NSString *)buildTime; @interface RCTPushyOrchestrator : NSObject + (void)persistConfiguration:(NSString *)config; + (BOOL)hasRunnableConfig; -+ (NSDictionary *)checkAndUpdate; ++ (NSDictionary *)prepareBundle; + (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion; #if TARGET_OS_TV && !DEBUG + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion @@ -1132,7 +1132,7 @@ + (void)configure:(NSDictionary *)options }); } -+ (void)checkAndUpdateWithCompletion:(RCTPushyNativeUpdateCompletion)completion ++ (void)prepareBundleWithCompletion:(RCTPushyBundlePreparationCompletion)completion { static dispatch_queue_t hostQueue; static dispatch_once_t once; @@ -1142,7 +1142,7 @@ + (void)checkAndUpdateWithCompletion:(RCTPushyNativeUpdateCompletion)completion dispatch_async(hostQueue, ^{ NSDictionary *result; @try { - result = [RCTPushyOrchestrator checkAndUpdate]; + result = [RCTPushyOrchestrator prepareBundle]; } @catch (NSException *exception) { RCTLogWarn(@"RCTPushy -- native host check failed: %@", exception.reason); result = PushyHostResult(@"failed", @"internal_error", nil, NO); @@ -2425,7 +2425,7 @@ + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion } #endif -+ (NSDictionary *)checkAndUpdate { ++ (NSDictionary *)prepareBundle { #if DEBUG return PushyHostResult(@"skipped", @"debug", nil, NO); #else diff --git a/src/__tests__/nativeConfiguration.test.ts b/src/__tests__/nativeConfiguration.test.ts index 2e6e0c42..859b809c 100644 --- a/src/__tests__/nativeConfiguration.test.ts +++ b/src/__tests__/nativeConfiguration.test.ts @@ -1,8 +1,8 @@ import { describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; import { runInNewContext } from 'node:vm'; -import type { NativeUpdateConfig } from '../../harmony/pushy/src/main/ets/NativeUpdateConfig'; -import { normalizeNativeUpdateConfig } from '../../harmony/pushy/src/main/ets/NativeUpdateConfig'; +import type { PushyConfiguration } from '../../harmony/pushy/src/main/ets/PushyConfiguration'; +import { normalizePushyConfiguration } from '../../harmony/pushy/src/main/ets/PushyConfiguration'; function runtimeSource(relativePath: string): string { const source = readFileSync(new URL(relativePath, import.meta.url), 'utf8') @@ -96,7 +96,7 @@ function clientHarness(native: boolean) { describe('native configuration normalization', () => { test('appKey alone supplies Pushy endpoints without automatically activating', () => { const config = JSON.parse( - normalizeNativeUpdateConfig({ appKey: 'test-app' }) + normalizePushyConfiguration({ appKey: 'test-app' }) ); expect(config.endpoints).toEqual([ 'https://update.react-native.cn/api', @@ -109,7 +109,7 @@ describe('native configuration normalization', () => { }); test('custom endpoints do not inherit public discovery, and are deduplicated', () => { - const options: NativeUpdateConfig = { + const options: PushyConfiguration = { appKey: 'test-app', endpoints: [ 'https://updates.example/api/', @@ -118,7 +118,7 @@ describe('native configuration normalization', () => { afterDownload: 'setNeedUpdate', }; const original = JSON.stringify(options); - const config = JSON.parse(normalizeNativeUpdateConfig(options)); + const config = JSON.parse(normalizePushyConfiguration(options)); expect(config.endpoints).toEqual(['https://updates.example/api']); expect(config.queryUrls).toEqual([]); expect(config.afterDownload).toBe('setNeedUpdate'); @@ -127,7 +127,7 @@ describe('native configuration normalization', () => { test('allows explicit discovery URLs and version identity overrides', () => { const config = JSON.parse( - normalizeNativeUpdateConfig({ + normalizePushyConfiguration({ appKey: 'test-app', endpoints: ['http://localhost:8080/api'], queryUrls: ['https://updates.example/endpoints.json?v=1'], @@ -164,7 +164,7 @@ describe('native configuration normalization', () => { ] as const) { test(`rejects ${name} before storage is touched`, () => { expect(() => - normalizeNativeUpdateConfig(options as unknown as NativeUpdateConfig) + normalizePushyConfiguration(options as unknown as PushyConfiguration) ).toThrow(); }); } diff --git a/src/__tests__/nativeHostApi.test.ts b/src/__tests__/nativeHostApi.test.ts index 4ff9951e..482326bb 100644 --- a/src/__tests__/nativeHostApi.test.ts +++ b/src/__tests__/nativeHostApi.test.ts @@ -1,11 +1,11 @@ import { describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; import { runInNewContext } from 'node:vm'; -import type { NativeUpdateResult } from '../../harmony/pushy/src/main/ets/NativeUpdateResult'; +import type { BundlePreparationResult } from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; import { + bundlePreparationResult, NativeUpdateRound, - nativeUpdateResult, -} from '../../harmony/pushy/src/main/ets/NativeUpdateResult'; +} from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; // Evaluate the actual Harmony orchestrator in an isolated VM per test. Only // platform imports, HTTP and download IO are substituted; entry points, @@ -68,10 +68,10 @@ function harness() { }, }; const runtime = runInNewContext( - `${javascript}\nrunCheckRequest = mockCheck;\nperformAttempts = mockDownload;\n({ check: checkAndUpdateNative, schedule: scheduleNativeCheck });`, + `${javascript}\nrunCheckRequest = mockCheck;\nperformAttempts = mockDownload;\n({ check: prepareBundleNative, schedule: scheduleNativeCheck });`, { NativeUpdateRound, - nativeUpdateResult, + bundlePreparationResult, logger: { info() {}, warn() {}, error() {} }, deviceInfo: { osFullName: 'test-os' }, setTimeout: (callback: () => void) => timers.push(callback), @@ -93,7 +93,7 @@ function harness() { }, } ) as { - check: (ctx: typeof context) => Promise; + check: (ctx: typeof context) => Promise; schedule: (ctx: typeof context, rollback: string) => void; }; return { @@ -138,7 +138,7 @@ describe('native host API orchestration', () => { for (const timer of h.timers) timer(); const results = await Promise.all([first, second]); expect(results[0]).toEqual( - nativeUpdateResult('downloaded', '', 'v2', true) + bundlePreparationResult('downloaded', '', 'v2', true) ); expect(results[1]).toEqual(results[0]); results[0].hash = 'caller-mutated'; @@ -153,7 +153,9 @@ describe('native host API orchestration', () => { const h = harness(); h.initialize(); h.state.decision = { action: 'download', hash: 'v2', activate: false }; - expect(await h.check()).toEqual(nativeUpdateResult('downloaded', '', 'v2')); + expect(await h.check()).toEqual( + bundlePreparationResult('downloaded', '', 'v2') + ); }); test('an installed version skips transfer but still reports activation', async () => { @@ -170,14 +172,14 @@ describe('native host API orchestration', () => { offline.initialize(); offline.state.reachable = false; expect(await offline.check()).toEqual( - nativeUpdateResult('failed', 'check_failed') + bundlePreparationResult('failed', 'check_failed') ); const h = harness(); h.initialize(); h.state.decision = { action: 'download', hash: 'v2' }; h.state.downloadOK = false; expect(await h.check()).toEqual( - nativeUpdateResult('failed', 'download_failed') + bundlePreparationResult('failed', 'download_failed') ); await h.check(); expect(h.state.downloads).toBe(1); @@ -189,7 +191,9 @@ describe('native host API orchestration', () => { h.state.beforeResponse = async () => { h.state.generation += 1; }; - expect(await h.check()).toEqual(nativeUpdateResult('cancelled', 'reset')); + expect(await h.check()).toEqual( + bundlePreparationResult('cancelled', 'reset') + ); }); test('reset and configuration changes invalidate completed snapshots', async () => { @@ -235,6 +239,6 @@ test('configuration replacement during an update cancels its returned snapshot', h.state.generation += 1; }; expect(await h.check()).toEqual( - nativeUpdateResult('cancelled', 'config_changed') + bundlePreparationResult('cancelled', 'config_changed') ); }); diff --git a/src/__tests__/nativeHostApiNaming.test.ts b/src/__tests__/nativeHostApiNaming.test.ts index 3b8fdce9..45509030 100644 --- a/src/__tests__/nativeHostApiNaming.test.ts +++ b/src/__tests__/nativeHostApiNaming.test.ts @@ -7,7 +7,8 @@ const harmony = 'harmony/pushy/src/main/ets/'; const source = (path: string) => readFileSync(new URL(path, root), 'utf8'); // These are forbidden host API identifiers, not backend paths or JS bridge names. -const oldNames = /\b(?:PushyNativeUpdate|NativeUpdateResult|NativeUpdateConfig|RCTPushyNativeUpdateCompletion|checkAndUpdate(?:WithCompletion|Native)?)\b/; +const oldNames = + /\b(?:PushyNativeUpdate|NativeUpdateResult|NativeUpdateConfig|RCTPushyNativeUpdateCompletion|checkAndUpdate(?:WithCompletion|Native)?)\b/; describe('native host API naming', () => { test('Android exposes prepareBundle through PushyRuntime', () => { @@ -16,9 +17,17 @@ describe('native host API naming', () => { expect(entry).toContain('public static void prepareBundle('); expect(entry).toContain('NativeCheckOrchestrator.prepareBundle('); expect(entry).toContain('void onComplete(BundlePreparationResult result)'); - expect(source(`${android}BundlePreparationResult.java`)).toContain('class BundlePreparationResult'); - expect(source(`${android}PushyConfiguration.java`)).toContain('class PushyConfiguration'); - for (const name of ['PushyNativeUpdate', 'NativeUpdateResult', 'NativeUpdateConfig']) { + expect(source(`${android}BundlePreparationResult.java`)).toContain( + 'class BundlePreparationResult' + ); + expect(source(`${android}PushyConfiguration.java`)).toContain( + 'class PushyConfiguration' + ); + for (const name of [ + 'PushyNativeUpdate', + 'NativeUpdateResult', + 'NativeUpdateConfig', + ]) { expect(existsSync(new URL(`${android}${name}.java`, root))).toBe(false); } }); @@ -38,8 +47,12 @@ describe('native host API naming', () => { test('Harmony exports the renamed result and configuration types', () => { const provider = source(`${harmony}PushyFileJSBundleProvider.ets`); const exports = source('harmony/pushy/index.ets'); - expect(provider).toContain('prepareBundle(): Promise'); - expect(provider).toContain('return prepareBundleNative(this.updateContext)'); + expect(provider).toContain( + 'prepareBundle(): Promise' + ); + expect(provider).toContain( + 'return prepareBundleNative(this.updateContext)' + ); expect(provider).toContain('configure(options: PushyConfiguration)'); expect(exports).toContain('export type { BundlePreparationResult }'); expect(exports).toContain('export type { PushyConfiguration }'); diff --git a/src/__tests__/nativeUpdateRound.test.ts b/src/__tests__/nativeUpdateRound.test.ts index d1f3f207..607a470a 100644 --- a/src/__tests__/nativeUpdateRound.test.ts +++ b/src/__tests__/nativeUpdateRound.test.ts @@ -1,18 +1,18 @@ import { describe, expect, test } from 'bun:test'; -import type { NativeUpdateResult } from '../../harmony/pushy/src/main/ets/NativeUpdateResult'; +import type { BundlePreparationResult } from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; import { + bundlePreparationResult, NativeUpdateRound, - nativeUpdateResult, -} from '../../harmony/pushy/src/main/ets/NativeUpdateResult'; +} from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; describe('native host update round', () => { test('concurrent callers share the same in-flight operation', async () => { const round = new NativeUpdateRound(); let calls = 0; - let complete: (result: NativeUpdateResult) => void = () => {}; + let complete: (result: BundlePreparationResult) => void = () => {}; const operation = () => { calls += 1; - return new Promise((resolve) => { + return new Promise((resolve) => { complete = resolve; }); }; @@ -21,7 +21,7 @@ describe('native host update round', () => { expect(second).toBe(first); await Promise.resolve(); expect(calls).toBe(1); - const result = nativeUpdateResult('downloaded', '', 'version-1', true); + const result = bundlePreparationResult('downloaded', '', 'version-1', true); complete(result); expect(await first).toEqual(result); expect(await second).toEqual(result); @@ -31,12 +31,12 @@ describe('native host update round', () => { test('the promise is published before a reentrant caller runs', async () => { const round = new NativeUpdateRound(); - let nested: Promise | undefined; + let nested: Promise | undefined; const first = round.run(async () => { nested = round.run(async () => { throw new Error('a second operation must not execute'); }); - return nativeUpdateResult('noUpdate', 'up_to_date'); + return bundlePreparationResult('noUpdate', 'up_to_date'); }); await first; expect(nested).toBe(first); @@ -47,7 +47,7 @@ describe('native host update round', () => { let calls = 0; const operation = async () => { calls += 1; - return nativeUpdateResult('failed', 'download_failed'); + return bundlePreparationResult('failed', 'download_failed'); }; expect((await round.run(operation)).status).toBe('failed'); expect((await round.run(operation)).reason).toBe('download_failed'); @@ -57,7 +57,7 @@ describe('native host update round', () => { test('unexpected rejection also cannot start a second round', async () => { const round = new NativeUpdateRound(); let calls = 0; - const operation = async (): Promise => { + const operation = async (): Promise => { calls += 1; throw new Error('transport unavailable'); }; @@ -68,16 +68,16 @@ describe('native host update round', () => { }); test('download and activation are separate facts', () => { - expect(nativeUpdateResult('downloaded', '', 'version-1')).toEqual({ + expect(bundlePreparationResult('downloaded', '', 'version-1')).toEqual({ status: 'downloaded', reason: '', hash: 'version-1', activated: false, }); expect( - nativeUpdateResult('downloaded', '', 'version-1', true).activated + bundlePreparationResult('downloaded', '', 'version-1', true).activated ).toBe(true); - expect(nativeUpdateResult('skipped', 'not_configured')).toEqual({ + expect(bundlePreparationResult('skipped', 'not_configured')).toEqual({ status: 'skipped', reason: 'not_configured', hash: '', From fd411ff03ab5a69138b8c9e3270895b1b0b03e59 Mon Sep 17 00:00:00 2001 From: Sunny Luo Date: Mon, 28 Sep 2026 16:39:15 +0800 Subject: [PATCH 5/6] chore: remove completed branch-only refactor workflow --- .../workflows/native-host-api-refactor.yml | 125 ------------------ 1 file changed, 125 deletions(-) delete mode 100644 .github/workflows/native-host-api-refactor.yml diff --git a/.github/workflows/native-host-api-refactor.yml b/.github/workflows/native-host-api-refactor.yml deleted file mode 100644 index 565b74bc..00000000 --- a/.github/workflows/native-host-api-refactor.yml +++ /dev/null @@ -1,125 +0,0 @@ -name: Prepare native host API refactor - -on: - push: - branches: - - refactor/native-host-prepare-bundle - paths: - - .github/workflows/native-host-api-refactor.yml - -permissions: - contents: write - -concurrency: - group: native-host-api-refactor-${{ github.ref }} - cancel-in-progress: false - -jobs: - refactor: - if: github.repository == 'reactnativecn/react-native-update' && github.ref == 'refs/heads/refactor/native-host-prepare-bundle' - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 - with: - bun-version: 1.4.0 - - - name: Rename native host identifiers and matching source files - id: rename - shell: bash - run: | - python3 - <<'PY' - from collections import Counter - from pathlib import Path - import re - import subprocess - - names = { - 'checkAndUpdateWithCompletion': 'prepareBundleWithCompletion', - 'checkAndUpdateNative': 'prepareBundleNative', - 'checkAndUpdate': 'prepareBundle', - 'RCTPushyNativeUpdateCompletion': 'RCTPushyBundlePreparationCompletion', - 'PushyNativeUpdate': 'PushyRuntime', - 'NativeUpdateResult': 'BundlePreparationResult', - 'nativeUpdateResult': 'bundlePreparationResult', - 'NativeUpdateConfig': 'PushyConfiguration', - 'normalizeNativeUpdateConfig': 'normalizePushyConfiguration', - } - pattern = re.compile(r'\b(?:' + '|'.join(map(re.escape, sorted(names, key=len, reverse=True))) + r')\b') - excluded = { - 'NATIVE_HOST_API_MIGRATION.md', - 'src/__tests__/nativeHostApiNaming.test.ts', - } - android = Path('android/src/main/java/cn/reactnative/modules/update') - assert (android / 'PushyNativeUpdate.java').is_file(), 'Expected the original host API' - bridge = Path('src/NativePushy.ts').read_bytes() - package = Path('package.json').read_bytes() - lock = Path('bun.lock').read_bytes() - counts = Counter() - tracked = subprocess.check_output(['git', 'ls-files', '-z']).decode().split('\0') - for relative in tracked: - if not relative or relative in excluded or relative.startswith('.github/'): - continue - path = Path(relative) - if not path.is_file() or path.is_symlink(): - continue - original = path.read_bytes() - if b'\0' in original: - continue - try: - text = original.decode('utf-8') - except UnicodeDecodeError: - continue - def replace(match): - counts[match.group()] += 1 - return names[match.group()] - replaced = pattern.sub(replace, text) - if replaced != text: - path.write_bytes(replaced.encode('utf-8')) - print('edited', relative) - if path.stem in names: - destination = path.with_name(names[path.stem] + path.suffix) - assert not destination.exists(), f'Refusing to overwrite {destination}' - path.rename(destination) - print('renamed', relative, '->', destination) - assert all(counts[name] > 0 for name in names), counts - assert Path('src/NativePushy.ts').read_bytes() == bridge, 'JS bridge ABI must remain unchanged' - assert Path('package.json').read_bytes() == package, 'Do not change package identity/version' - assert Path('bun.lock').read_bytes() == lock, 'Do not change dependencies' - for filename, section in { - 'README.md': '\n## Native host APIs\n\nBridge-free hosts use `PushyRuntime.prepareBundle` (Android),\n`RCTPushy.prepareBundle` (Swift), or `PushyFileJSBundleProvider.prepareBundle`\n(HarmonyOS), after normal launch bundle resolution. See the\n[native host API migration guide](NATIVE_HOST_API_MIGRATION.md) for the\nsource-breaking rename, unchanged JS bridge contract, and native rebuild steps.\n', - 'README-CN.md': '\n## 原生宿主接口\n\n原生宿主入口统一为 `prepareBundle`:Android 使用 `PushyRuntime`,\nApple 平台使用 `RCTPushy`,Harmony 使用已有的 `PushyFileJSBundleProvider`。\n请在正常启动 bundle 解析后调用。改名不改变 JS 桥接契约,但原生接入代码需要迁移并重新构建。\n详见[原生接口迁移说明](NATIVE_HOST_API_MIGRATION.md)。\n', - }.items(): - with Path(filename).open('a', encoding='utf-8') as stream: - stream.write(section) - print('Replacement counts:', dict(counts)) - PY - - - name: Install locked dependencies - run: bun install --frozen-lockfile - - - name: Format changed JavaScript tests - shell: bash - run: | - mapfile -t changed < <(git diff --name-only --diff-filter=ACM -- src | grep -E '\.(ts|tsx)$' || true) - ./node_modules/.bin/biome check --write src/__tests__/nativeHostApiNaming.test.ts "${changed[@]}" - - - name: Run unit tests - run: bun test src/__tests__ - - - name: Run lint, TypeScript and native parity checks - if: success() || failure() - run: bun run lint - - - name: Commit source changes to this feature branch only - if: always() && steps.rename.outcome == 'success' - shell: bash - run: | - test "${GITHUB_REF}" = 'refs/heads/refactor/native-host-prepare-bundle' - git diff --exit-code -- package.json bun.lock src/NativePushy.ts .github - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add -A - git commit -m 'refactor!: rename bridge-free native host APIs to bundle preparation' - git push origin HEAD:refs/heads/refactor/native-host-prepare-bundle From bb306a420d5b411899ca5e7cabb76f1dfa6b3c7b Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 28 Sep 2026 17:11:38 +0800 Subject: [PATCH 6/6] refactor!: finish native-update naming cleanup for host APIs added after 10.56.0 - iOS: RCTPushyNativeConfigurationCompletion -> RCTPushyConfigurationCompletion, RCTPushyNormalizeNativeConfig -> RCTPushyNormalizeConfiguration, RCTPushyNativeConfig.{h,mm} -> RCTPushyConfiguration.{h,mm} - Harmony: NativeUpdateRound -> BundlePreparationRound, PersistedNativeUpdateConfig -> PersistedPushyConfiguration - Android/Harmony: "configure native update" error text -> "persist configuration" - Naming test now scans every shipped native source for old host names and "native update" wording (JNI-bound NativeUpdateCore/Flow excepted) Co-Authored-By: Claude Opus 5.5 --- NATIVE_HOST_API_MIGRATION.md | 4 +- .../modules/update/UpdateContext.java | 2 +- .../src/main/ets/BundlePreparationResult.ts | 2 +- .../src/main/ets/NativeCheckOrchestrator.ts | 4 +- .../pushy/src/main/ets/PushyConfiguration.ts | 4 +- harmony/pushy/src/main/ets/UpdateContext.ts | 2 +- ios/RCTPushy/RCTPushy.h | 10 ++-- ios/RCTPushy/RCTPushy.mm | 6 +-- ...NativeConfig.h => RCTPushyConfiguration.h} | 2 +- ...tiveConfig.mm => RCTPushyConfiguration.mm} | 4 +- ...test.ts => bundlePreparationRound.test.ts} | 12 ++--- src/__tests__/nativeHostApi.test.ts | 4 +- src/__tests__/nativeHostApiNaming.test.ts | 53 +++++++++++++++---- 13 files changed, 71 insertions(+), 38 deletions(-) rename ios/RCTPushy/{RCTPushyNativeConfig.h => RCTPushyConfiguration.h} (72%) rename ios/RCTPushy/{RCTPushyNativeConfig.mm => RCTPushyConfiguration.mm} (97%) rename src/__tests__/{nativeUpdateRound.test.ts => bundlePreparationRound.test.ts} (91%) diff --git a/NATIVE_HOST_API_MIGRATION.md b/NATIVE_HOST_API_MIGRATION.md index eabe4835..3b79d6a2 100644 --- a/NATIVE_HOST_API_MIGRATION.md +++ b/NATIVE_HOST_API_MIGRATION.md @@ -14,6 +14,7 @@ or crash-rescue behavior. No deprecated native alias is retained. | 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 @@ -53,7 +54,8 @@ the API names that existed in the versions they document. 原生宿主入口统一为 `prepareBundle`,Android 对外类改为 `PushyRuntime`, 结果类型改为 `BundlePreparationResult`,Harmony 配置类型改为 -`PushyConfiguration`。iOS/Swift 的方法与完成回调类型同步改名,不保留旧名别名。 +`PushyConfiguration`。iOS/Swift 的方法与完成回调类型(含 `configure` 的 +`RCTPushyConfigurationCompletion`)同步改名,不保留旧名别名。 这是原生源码接口的破坏性重命名,不改变下载、下次启动选包、回滚或救援行为。 配置完成后,先走宿主正常的 bundle 解析流程,再调用 `prepareBundle`; diff --git a/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java b/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java index e5ba1432..5d39abb8 100644 --- a/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +++ b/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java @@ -766,7 +766,7 @@ void setNativeConfig(String config) { editor.putString(NativeCheckOrchestrator.KEY_CONFIG, config); // Persist even an equal value: a previous commit may have updated // SharedPreferences memory but failed to write its file. - persistEditorOrThrow(editor, "configure native update"); + persistEditorOrThrow(editor, "persist configuration"); } NativeCheckOrchestrator.onConfigured(this); } diff --git a/harmony/pushy/src/main/ets/BundlePreparationResult.ts b/harmony/pushy/src/main/ets/BundlePreparationResult.ts index e50eee04..7c425fa5 100644 --- a/harmony/pushy/src/main/ets/BundlePreparationResult.ts +++ b/harmony/pushy/src/main/ets/BundlePreparationResult.ts @@ -18,7 +18,7 @@ export function bundlePreparationResult( } /** Internal gate shared by the delayed check and the native host API. */ -export class NativeUpdateRound { +export class BundlePreparationRound { private task: Promise | undefined; run(operation: () => Promise): Promise { diff --git a/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts b/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts index 44b7b775..cbeff9ee 100644 --- a/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts +++ b/harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts @@ -3,7 +3,7 @@ import deviceInfo from '@ohos.deviceInfo'; import logger from './Logger'; import NativePatchCore from './NativePatchCore'; import type { UpdateContext } from './UpdateContext'; -import { NativeUpdateRound, bundlePreparationResult } from './BundlePreparationResult'; +import { BundlePreparationRound, bundlePreparationResult } from './BundlePreparationResult'; import type { BundlePreparationResult } from './BundlePreparationResult'; import { isSafePathComponent } from './PathUtils'; import { monotonicNowMs } from './MonotonicClock'; @@ -109,7 +109,7 @@ interface RespCacheEntry { let scheduled = false; // The host and delayed check use one promise, including its settled result. -const hostRound = new NativeUpdateRound(); +const hostRound = new BundlePreparationRound(); let scheduledContext: UpdateContext | undefined; let scheduledRollback = ''; let roundGeneration = -1; diff --git a/harmony/pushy/src/main/ets/PushyConfiguration.ts b/harmony/pushy/src/main/ets/PushyConfiguration.ts index 76c0b64c..4c846068 100644 --- a/harmony/pushy/src/main/ets/PushyConfiguration.ts +++ b/harmony/pushy/src/main/ets/PushyConfiguration.ts @@ -14,7 +14,7 @@ export interface PushyConfiguration { rn?: string; } -interface PersistedNativeUpdateConfig { +interface PersistedPushyConfiguration { appKey: string; endpoints: string[]; queryUrls: string[]; @@ -90,7 +90,7 @@ export function normalizePushyConfiguration(options: PushyConfiguration): string if (options.disabled !== undefined && typeof options.disabled !== 'boolean') { throw new Error('Invalid native configuration: disabled must be a boolean'); } - const result: PersistedNativeUpdateConfig = { + const result: PersistedPushyConfiguration = { appKey, endpoints, queryUrls, afterDownload, disabled: options.disabled ?? false, rnu: options.rnu === undefined ? '' : configString(options.rnu, 'rnu', true), rn: options.rn === undefined ? '' : configString(options.rn, 'rn', true), diff --git a/harmony/pushy/src/main/ets/UpdateContext.ts b/harmony/pushy/src/main/ets/UpdateContext.ts index 8380bd4b..ad9eea8f 100644 --- a/harmony/pushy/src/main/ets/UpdateContext.ts +++ b/harmony/pushy/src/main/ets/UpdateContext.ts @@ -480,7 +480,7 @@ export class UpdateContext { markJsCheckCompleted(''); } // Flush even an equal value so a retry after a storage error can succeed. - return this.flushPreferences('configure native update'); + return this.flushPreferences('persist configuration'); } public setKv(key: string, value: string): Promise { diff --git a/ios/RCTPushy/RCTPushy.h b/ios/RCTPushy/RCTPushy.h index e41a2945..21f225ca 100644 --- a/ios/RCTPushy/RCTPushy.h +++ b/ios/RCTPushy/RCTPushy.h @@ -1,7 +1,7 @@ #import #import -typedef void (^RCTPushyNativeConfigurationCompletion)(NSError * _Nullable error); +typedef void (^RCTPushyConfigurationCompletion)(NSError * _Nullable error); typedef void (^RCTPushyBundlePreparationCompletion)(NSDictionary * _Nonnull result); @@ -14,13 +14,13 @@ typedef void (^RCTPushyBundlePreparationCompletion)(NSDictionary * normal launch bundle resolution for first-install native-only provisioning. */ + (void)configure:(NSDictionary * _Nonnull)options - completion:(RCTPushyNativeConfigurationCompletion _Nullable)completion + completion:(RCTPushyConfigurationCompletion _Nullable)completion NS_SWIFT_NAME(configure(_:completion:)); /** - * Start, join, or reuse this process's native update round. Call after the - * host's real bundleURL resolution; this method never resolves the bundle - * again, reloads React Native, or displays UI. Configuration is the one + * Start, join, or reuse this process's native bundle-preparation round. Call + * after the host's real bundleURL resolution; this method never resolves the + * bundle again, reloads React Native, or displays UI. Configuration is the one * persisted by the JS SDK. The optional completion runs on the main queue. * * Result keys: status (skipped/noUpdate/downloaded/failed/cancelled), reason, diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index 1327c73f..5bbfd8c4 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -1,5 +1,5 @@ #import "RCTPushy.h" -#import "RCTPushyNativeConfig.h" +#import "RCTPushyConfiguration.h" #import "RCTPushyDownloader.h" #import "ZipArchive.h" #include "../../cpp/patch_core/archive_limits.h" @@ -1104,11 +1104,11 @@ + (NSString *) rollback { } + (void)configure:(NSDictionary *)options - completion:(RCTPushyNativeConfigurationCompletion)completion + completion:(RCTPushyConfigurationCompletion)completion { NSError *validationError = nil; // Snapshot nested mutable caller values before crossing a queue boundary. - NSString *config = RCTPushyNormalizeNativeConfig(options, &validationError); + NSString *config = RCTPushyNormalizeConfiguration(options, &validationError); static dispatch_queue_t configQueue; static dispatch_once_t once; dispatch_once(&once, ^{ diff --git a/ios/RCTPushy/RCTPushyNativeConfig.h b/ios/RCTPushy/RCTPushyConfiguration.h similarity index 72% rename from ios/RCTPushy/RCTPushyNativeConfig.h rename to ios/RCTPushy/RCTPushyConfiguration.h index 99b99459..8231b68c 100644 --- a/ios/RCTPushy/RCTPushyNativeConfig.h +++ b/ios/RCTPushy/RCTPushyConfiguration.h @@ -1,5 +1,5 @@ #import // Validates and snapshots host options without changing any update state. -FOUNDATION_EXPORT NSString * _Nullable RCTPushyNormalizeNativeConfig( +FOUNDATION_EXPORT NSString * _Nullable RCTPushyNormalizeConfiguration( NSDictionary * _Nonnull options, NSError * _Nullable * _Nullable error); diff --git a/ios/RCTPushy/RCTPushyNativeConfig.mm b/ios/RCTPushy/RCTPushyConfiguration.mm similarity index 97% rename from ios/RCTPushy/RCTPushyNativeConfig.mm rename to ios/RCTPushy/RCTPushyConfiguration.mm index 4303372c..2133b2cc 100644 --- a/ios/RCTPushy/RCTPushyNativeConfig.mm +++ b/ios/RCTPushy/RCTPushyConfiguration.mm @@ -1,4 +1,4 @@ -#import "RCTPushyNativeConfig.h" +#import "RCTPushyConfiguration.h" static void PushyConfigInvalid(NSString *message) { @throw [NSException exceptionWithName:NSInvalidArgumentException @@ -48,7 +48,7 @@ static void PushyConfigInvalid(NSString *message) { return result; } -NSString *RCTPushyNormalizeNativeConfig(NSDictionary *options, NSError **error) { +NSString *RCTPushyNormalizeConfiguration(NSDictionary *options, NSError **error) { @try { if (![options isKindOfClass:NSDictionary.class]) { PushyConfigInvalid(@"expected an object"); diff --git a/src/__tests__/nativeUpdateRound.test.ts b/src/__tests__/bundlePreparationRound.test.ts similarity index 91% rename from src/__tests__/nativeUpdateRound.test.ts rename to src/__tests__/bundlePreparationRound.test.ts index 607a470a..151026ae 100644 --- a/src/__tests__/nativeUpdateRound.test.ts +++ b/src/__tests__/bundlePreparationRound.test.ts @@ -1,13 +1,13 @@ import { describe, expect, test } from 'bun:test'; import type { BundlePreparationResult } from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; import { + BundlePreparationRound, bundlePreparationResult, - NativeUpdateRound, } from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; -describe('native host update round', () => { +describe('bundle preparation round', () => { test('concurrent callers share the same in-flight operation', async () => { - const round = new NativeUpdateRound(); + const round = new BundlePreparationRound(); let calls = 0; let complete: (result: BundlePreparationResult) => void = () => {}; const operation = () => { @@ -30,7 +30,7 @@ describe('native host update round', () => { }); test('the promise is published before a reentrant caller runs', async () => { - const round = new NativeUpdateRound(); + const round = new BundlePreparationRound(); let nested: Promise | undefined; const first = round.run(async () => { nested = round.run(async () => { @@ -43,7 +43,7 @@ describe('native host update round', () => { }); test('a failed round is reused rather than causing a retry storm', async () => { - const round = new NativeUpdateRound(); + const round = new BundlePreparationRound(); let calls = 0; const operation = async () => { calls += 1; @@ -55,7 +55,7 @@ describe('native host update round', () => { }); test('unexpected rejection also cannot start a second round', async () => { - const round = new NativeUpdateRound(); + const round = new BundlePreparationRound(); let calls = 0; const operation = async (): Promise => { calls += 1; diff --git a/src/__tests__/nativeHostApi.test.ts b/src/__tests__/nativeHostApi.test.ts index 482326bb..12230f17 100644 --- a/src/__tests__/nativeHostApi.test.ts +++ b/src/__tests__/nativeHostApi.test.ts @@ -3,8 +3,8 @@ import { readFileSync } from 'node:fs'; import { runInNewContext } from 'node:vm'; import type { BundlePreparationResult } from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; import { + BundlePreparationRound, bundlePreparationResult, - NativeUpdateRound, } from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; // Evaluate the actual Harmony orchestrator in an isolated VM per test. Only @@ -70,7 +70,7 @@ function harness() { const runtime = runInNewContext( `${javascript}\nrunCheckRequest = mockCheck;\nperformAttempts = mockDownload;\n({ check: prepareBundleNative, schedule: scheduleNativeCheck });`, { - NativeUpdateRound, + BundlePreparationRound, bundlePreparationResult, logger: { info() {}, warn() {}, error() {} }, deviceInfo: { osFullName: 'test-os' }, diff --git a/src/__tests__/nativeHostApiNaming.test.ts b/src/__tests__/nativeHostApiNaming.test.ts index 45509030..b0308d60 100644 --- a/src/__tests__/nativeHostApiNaming.test.ts +++ b/src/__tests__/nativeHostApiNaming.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from 'bun:test'; -import { existsSync, readFileSync } from 'node:fs'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; const root = new URL('../../', import.meta.url); const android = 'android/src/main/java/cn/reactnative/modules/update/'; @@ -8,7 +8,23 @@ const source = (path: string) => readFileSync(new URL(path, root), 'utf8'); // These are forbidden host API identifiers, not backend paths or JS bridge names. const oldNames = - /\b(?:PushyNativeUpdate|NativeUpdateResult|NativeUpdateConfig|RCTPushyNativeUpdateCompletion|checkAndUpdate(?:WithCompletion|Native)?)\b/; + /\b(?:PushyNativeUpdate|NativeUpdateResult|NativeUpdateConfig|RCTPushyNativeUpdateCompletion|RCTPushyNativeConfigurationCompletion|RCTPushyNormalizeNativeConfig|checkAndUpdate(?:WithCompletion|Native)?)\b/; +// Any other "native update" wording in shipped native code. NativeUpdateCore and +// NativeUpdateFlow predate the host APIs and are bound by JNI symbol names. +const nativeUpdateWording = /NativeUpdate(?!Core|Flow)|native update/i; + +const shippedNativeSources = (dir: string): string[] => + readdirSync(new URL(dir, root), { recursive: true, withFileTypes: true }) + .filter( + (entry) => + entry.isFile() && /\.(?:h|m|mm|java|kt|ts|ets|cpp)$/.test(entry.name) + ) + .map((entry) => + `${entry.parentPath}/${entry.name}`.slice( + new URL(dir, root).pathname.length - dir.length + ) + ) + .filter((path) => !/\/tests?\//.test(path)); describe('native host API naming', () => { test('Android exposes prepareBundle through PushyRuntime', () => { @@ -63,15 +79,30 @@ describe('native host API naming', () => { } }); - test('renamed host entry files contain no deprecated native aliases', () => { - for (const path of [ - `${android}PushyRuntime.java`, - `${android}BundlePreparationResult.java`, - `${android}PushyConfiguration.java`, - `${harmony}BundlePreparationResult.ts`, - `${harmony}PushyConfiguration.ts`, - ]) { - expect(source(path)).not.toMatch(oldNames); + test('iOS configuration names match the other platforms', () => { + const header = source('ios/RCTPushy/RCTPushy.h'); + expect(header).toContain('typedef void (^RCTPushyConfigurationCompletion)'); + expect(source('ios/RCTPushy/RCTPushyConfiguration.h')).toContain( + 'RCTPushyNormalizeConfiguration(' + ); + for (const name of ['RCTPushyNativeConfig.h', 'RCTPushyNativeConfig.mm']) { + expect(existsSync(new URL(`ios/RCTPushy/${name}`, root))).toBe(false); } }); + + test('shipped native sources contain no old host names or native-update wording', () => { + const paths = [ + 'ios/', + 'android/src/main/', + 'harmony/pushy/src/main/', + 'cpp/patch_core/', + 'cpp/update_flow_core/', + ].flatMap(shippedNativeSources); + expect(paths).toContain(`${android}PushyRuntime.java`); + const offenders = paths.filter((path) => { + const text = source(path); + return oldNames.test(text) || nativeUpdateWording.test(text); + }); + expect(offenders).toEqual([]); + }); });