From 6bee0c44bd18f55e72a920e489a718a1bb2e0249 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:14:32 +0800 Subject: [PATCH 01/21] ci: pin GitHub Actions to commit SHAs again a9793d6e moved every action back to floating major tags while bumping majors, which left publish.yml (id-token: write) trusting whatever a tag points at on release day. Pin each one to the commit its current major tag resolves to, with the exact release in a trailing comment so Dependabot keeps the SHA/version pairs current (as dependabot.yml says). Co-Authored-By: Claude Opus 5.5 --- .github/actions/e2e-cache/action.yml | 4 +-- .github/workflows/e2e_android.yml | 46 ++++++++++++++-------------- .github/workflows/e2e_harmony.yml | 6 ++-- .github/workflows/e2e_ios.yml | 20 ++++++------ .github/workflows/harmony-build.yml | 6 ++-- .github/workflows/lint.yml | 4 +-- .github/workflows/native-gate.yml | 2 +- .github/workflows/publish.yml | 22 ++++++------- .github/workflows/test.yml | 8 ++--- 9 files changed, 59 insertions(+), 59 deletions(-) diff --git a/.github/actions/e2e-cache/action.yml b/.github/actions/e2e-cache/action.yml index d598441a..1352259f 100644 --- a/.github/actions/e2e-cache/action.yml +++ b/.github/actions/e2e-cache/action.yml @@ -18,7 +18,7 @@ runs: run: echo "day=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" - name: Restore read-only snapshot if: "!(inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios')" - uses: actions/cache/restore@v6 + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ inputs.path }} key: e2e-content-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.scope }}-${{ steps.epoch.outputs.day }} @@ -26,7 +26,7 @@ runs: e2e-content-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.scope }}- - name: Restore and save trusted daily snapshot if: inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios' - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ inputs.path }} key: e2e-content-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.scope }}-${{ steps.epoch.outputs.day }} diff --git a/.github/workflows/e2e_android.yml b/.github/workflows/e2e_android.yml index 1a1796b3..9e9800f4 100644 --- a/.github/workflows/e2e_android.yml +++ b/.github/workflows/e2e_android.yml @@ -45,28 +45,28 @@ jobs: timeout-minutes: 30 steps: - name: Checkout react-native-update - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Checkout react-native-update-cli - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: reactnativecn/react-native-update-cli path: react-native-update-cli - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 - name: Cache Bun dependencies - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-e2e-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }} @@ -74,7 +74,7 @@ jobs: ${{ runner.os }}-bun-e2e- - name: Setup Java - uses: actions/setup-java@v6 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: temurin java-version: '17' @@ -122,7 +122,7 @@ jobs: run: cd Example/e2etest && bun run prepare:e2e - name: Detox test (android.emu.release) - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 env: DETOX_ANDROID_ARCHS: x86_64 RNU_CLI_ROOT: ${{ github.workspace }}/react-native-update-cli @@ -152,7 +152,7 @@ jobs: - name: Upload Detox artifacts # cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场 if: failure() || cancelled() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: e2e-android-detox-artifacts path: Example/e2etest/artifacts @@ -168,22 +168,22 @@ jobs: timeout-minutes: 30 steps: - name: Checkout react-native-update - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 - name: Cache Bun dependencies - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-e2e-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }} @@ -191,7 +191,7 @@ jobs: ${{ runner.os }}-bun-e2e- - name: Setup Java - uses: actions/setup-java@v6 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: temurin java-version: '17' @@ -239,7 +239,7 @@ jobs: nohup bash -c 'for i in $(seq 1 90); do curl -sf http://localhost:8081/status >/dev/null && break; sleep 2; done; curl -sf --max-time 420 -o /dev/null "http://localhost:8081/index.bundle?platform=android&dev=true&minify=false"' > .e2e-artifacts/metro-prewarm.log 2>&1 & - name: Detox test (android.emu.debug boot smoke) - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 env: DETOX_ANDROID_ARCHS: x86_64 DETOX_AVD_NAME: api34 @@ -266,7 +266,7 @@ jobs: - name: Upload Detox artifacts # cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场 if: failure() || cancelled() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: e2e-android-debug-smoke-detox-artifacts path: | @@ -281,28 +281,28 @@ jobs: timeout-minutes: 35 steps: - name: Checkout react-native-update - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Checkout react-native-update-cli - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: reactnativecn/react-native-update-cli path: react-native-update-cli - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 - name: Cache Bun dependencies - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-e2e-rn077-oldarch-${{ hashFiles('Example/e2etest/bun.lock', 'Example/e2etest/scripts/create-rn077-oldarch-project.js', 'react-native-update-cli/bun.lock') }} @@ -311,7 +311,7 @@ jobs: ${{ runner.os }}-bun-e2e- - name: Setup Java - uses: actions/setup-java@v6 + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 with: distribution: temurin java-version: '17' @@ -360,7 +360,7 @@ jobs: run: cd .e2e-rn077-oldarch/AwesomeProject && bun run prepare:e2e - name: Detox test (RN 0.77.3 old arch android.emu.release) - uses: reactivecircus/android-emulator-runner@v2 + uses: reactivecircus/android-emulator-runner@a421e43855164a8197daf9d8d40fe71c6996bb0d # v2.38.0 env: DETOX_ANDROID_ARCHS: x86_64 RNU_CLI_ROOT: ${{ github.workspace }}/react-native-update-cli @@ -387,7 +387,7 @@ jobs: - name: Upload Detox artifacts # cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场 if: failure() || cancelled() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: e2e-android-rn077-oldarch-detox-artifacts path: .e2e-rn077-oldarch/AwesomeProject/artifacts diff --git a/.github/workflows/e2e_harmony.yml b/.github/workflows/e2e_harmony.yml index 61620624..47dc11a3 100644 --- a/.github/workflows/e2e_harmony.yml +++ b/.github/workflows/e2e_harmony.yml @@ -26,12 +26,12 @@ jobs: HVD_NAME: api20 steps: - name: Checkout react-native-update - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Checkout react-native-update-cli - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: reactnativecn/react-native-update-cli path: react-native-update-cli @@ -94,7 +94,7 @@ jobs: - name: Upload failure diagnostics # cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场 if: failure() || cancelled() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: harmony-e2e-diagnostics path: harmony-diagnostics/ diff --git a/.github/workflows/e2e_ios.yml b/.github/workflows/e2e_ios.yml index cc031572..468dabcb 100644 --- a/.github/workflows/e2e_ios.yml +++ b/.github/workflows/e2e_ios.yml @@ -65,28 +65,28 @@ jobs: jest-config: '--config e2e/native/jest.config.js' steps: - name: Checkout react-native-update - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Checkout react-native-update-cli - uses: actions/checkout@v7 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: reactnativecn/react-native-update-cli path: react-native-update-cli - name: Setup Node.js - uses: actions/setup-node@v7 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 22 - name: Setup Bun - uses: oven-sh/setup-bun@v2 + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 - name: Cache Bun dependencies - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/.bun/install/cache key: ${{ runner.os }}-bun-e2e-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }} @@ -94,7 +94,7 @@ jobs: ${{ runner.os }}-bun-e2e- - name: Setup Xcode - uses: maxim-lobanov/setup-xcode@v1 + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 with: xcode-version: '26.4' @@ -166,7 +166,7 @@ jobs: run: cd Example/e2etest && bun run prepare:e2e - name: Cache CocoaPods downloads - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/Library/Caches/CocoaPods key: ${{ runner.os }}-cocoapods-e2e-${{ hashFiles('Example/e2etest/ios/Podfile.lock', 'Example/e2etest/ios/Podfile', 'Example/e2etest/bun.lock') }} @@ -181,7 +181,7 @@ jobs: # 会在改 spec 后把新 JS 注入到旧胶水的二进制上。 # 故意不设 restore-keys:部分命中就是错的二进制,宁可全量重建。 id: cache-ios-app - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: | Example/e2etest/ios/build/Build/Products/Release-iphonesimulator/AwesomeProject.app @@ -196,7 +196,7 @@ jobs: - name: Cache Detox iOS framework id: cache-detox-ios-framework - uses: actions/cache@v6 + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ~/Library/Detox key: ${{ runner.os }}-detox-ios-framework-xcode-26.4-${{ hashFiles('Example/e2etest/bun.lock') }} @@ -229,7 +229,7 @@ jobs: - name: Upload Detox artifacts # cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场 if: failure() || cancelled() - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: e2e-ios-detox-artifacts-${{ matrix.suite }} path: Example/e2etest/artifacts diff --git a/.github/workflows/harmony-build.yml b/.github/workflows/harmony-build.yml index a04850b3..aa7ee4e0 100644 --- a/.github/workflows/harmony-build.yml +++ b/.github/workflows/harmony-build.yml @@ -50,19 +50,19 @@ jobs: container: ghcr.io/sanchuanhehe/harmony-next-pipeline-docker/harmonyos-ci-image:latest@sha256:2836142b7b6ae8837ad0e36cacc650f00e8c88f55b4e9f34be66cc98a731c5da timeout-minutes: 20 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Configure git safe.directory run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" git submodule foreach --recursive 'git config --global --add safe.directory "$toplevel/$sm_path"' - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 # The container ships no node; the check scripts and build-harmony-har # (npm run) need one, exactly as publish.yml sets it up. - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 - run: bun install --frozen-lockfile diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index c2bdab1c..09c1474e 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -20,8 +20,8 @@ jobs: timeout-minutes: 5 steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 diff --git a/.github/workflows/native-gate.yml b/.github/workflows/native-gate.yml index 051cc968..e08bc7bf 100644 --- a/.github/workflows/native-gate.yml +++ b/.github/workflows/native-gate.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 5 steps: - - uses: actions/github-script@v9 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: | const sha = context.payload.workflow_run.head_sha; diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index c90fe80a..be64c47f 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -17,7 +17,7 @@ jobs: harmony_changed: ${{ steps.check.outputs.changed }} prev_tag: ${{ steps.check.outputs.prev_tag }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Check Harmony changes @@ -69,7 +69,7 @@ jobs: # Keep in sync with PINNED_NDK_VERSION in scripts/build-android-so.sh. NDK_VERSION: 28.2.13676358 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Install pinned NDK @@ -80,7 +80,7 @@ jobs: # resolution order — force the pinned one so published .so files don't # drift with runner image upgrades. run: ANDROID_NDK_HOME="$ANDROID_HOME/ndk/$NDK_VERSION" bash scripts/build-android-so.sh - - uses: actions/upload-artifact@v7 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: name: android-so path: android/lib/ @@ -94,18 +94,18 @@ jobs: container: ghcr.io/sanchuanhehe/harmony-next-pipeline-docker/harmonyos-ci-image:latest@sha256:2836142b7b6ae8837ad0e36cacc650f00e8c88f55b4e9f34be66cc98a731c5da timeout-minutes: 15 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Configure git safe.directory run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" git submodule foreach --recursive 'git config --global --add safe.directory "$toplevel/$sm_path"' - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 registry-url: 'https://registry.npmjs.org' @@ -116,7 +116,7 @@ jobs: - name: Remove committed Android native libraries run: rm -rf android/lib - name: Use CI-built Android native libraries - uses: actions/download-artifact@v8 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: android-so path: android/lib @@ -145,18 +145,18 @@ jobs: runs-on: ubuntu-latest # npm --provenance requires a GitHub-hosted runner timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive - name: Configure git safe.directory run: | git config --global --add safe.directory "$GITHUB_WORKSPACE" git submodule foreach --recursive 'git config --global --add safe.directory "$toplevel/$sm_path"' - - uses: oven-sh/setup-bun@v2 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v7 + - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: 24 registry-url: 'https://registry.npmjs.org' @@ -167,7 +167,7 @@ jobs: - name: Remove committed Android native libraries run: rm -rf android/lib - name: Use CI-built Android native libraries - uses: actions/download-artifact@v8 + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 with: name: android-so path: android/lib diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 511a0cf9..6e15a93a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -21,8 +21,8 @@ jobs: timeout-minutes: 5 steps: - - uses: actions/checkout@v7 - - uses: oven-sh/setup-bun@v2 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 with: bun-version: 1.4.0 @@ -38,7 +38,7 @@ jobs: timeout-minutes: 10 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: submodules: recursive @@ -86,7 +86,7 @@ jobs: timeout-minutes: 5 steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 # Foundation-only executable: exercises the production tvOS Release # methods without RN, CocoaPods, a simulator, or real network delays. - name: Run deterministic native ordering tests and negative controls From dd46b75fc42a5a332a39dda67c60c0f2541f4865 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:16:20 +0800 Subject: [PATCH 02/21] fix(js): tighten native config sync, check cache reuse and retry paths - readNativeCheckCache: judge entry age before parsing anything else, compare configs with the shared canonical equality (C1: nativeConfigsEquivalent in updateFlowCore), and stop reading the cache for the rest of the process once it was seen absent or expired. - markJsCheckCompleted: send once per config after it succeeds. - syncNativeConfig: forget the synced value when ownership moves to 'native' so switching back re-writes; a NATIVE_CONFIG_CONFLICT rejection (host called native configure()) now console.errors an actionable message, goes through emitError/onError and stops retries. - markSuccess: share the in-flight native call between concurrent callers (one native call, one report); cleared once settled. - performDownload: HEAD-rank mirrors once per strategy per download; a failed winner is demoted instead of re-racing every attempt. - provider: the Linking effect reads parseTestPayload through a ref and handles the launch URL once, instead of replaying it on every strategy-callback identity change. - utils: web ping no longer passes an unbound Promise.resolve; inline createNonSecureId replaces nanoid/non-secure. Co-Authored-By: Claude Opus 5.5 --- src/__tests__/client.test.ts | 281 ++++++++++++++++++++++++- src/__tests__/core.test.ts | 10 +- src/__tests__/provider.render.test.tsx | 32 +++ src/__tests__/updateFlowCore.test.ts | 86 ++++++++ src/__tests__/utils.test.ts | 37 ++++ src/client.ts | 127 ++++++++--- src/core.ts | 4 +- src/error.ts | 6 +- src/locales/en.ts | 2 + src/locales/zh.ts | 2 + src/provider.tsx | 21 +- src/type.ts | 3 + src/updateFlowCore.ts | 99 +++++++++ src/utils.ts | 22 +- 14 files changed, 691 insertions(+), 41 deletions(-) diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index d146efa5..9c846ad4 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -475,6 +475,62 @@ describe('Pushy server config', () => { ); }); + test('concurrent markSuccess calls share one native call and one report', async () => { + let resolveNative = () => {}; + const nativeMarkSuccess = mock( + () => + new Promise((resolve) => { + resolveNative = resolve; + }) + ); + const logger = mock(() => {}); + setupClientMocks({ isFirstTime: true, markSuccess: nativeMarkSuccess }); + const { Pushy, sharedState } = await importFreshClient( + 'mark-success-concurrent' + ); + const client = new Pushy({ appKey: 'demo-app', logger }); + + const first = client.markSuccess(); + const second = client.markSuccess(); + resolveNative(); + await Promise.all([first, second]); + await client.markSuccess(); + + expect(nativeMarkSuccess).toHaveBeenCalledTimes(1); + expect(sharedState.marked).toBe(true); + expect( + logger.mock.calls.filter(([event]: any[]) => event.type === 'markSuccess') + ).toHaveLength(1); + }); + + test('a failed markSuccess can be retried', async () => { + let calls = 0; + const nativeMarkSuccess = mock(() => + ++calls === 1 ? Promise.reject(Error('busy')) : Promise.resolve() + ); + setupClientMocks({ isFirstTime: true, markSuccess: nativeMarkSuccess }); + const { Pushy, sharedState } = + await importFreshClient('mark-success-retry'); + const client = new Pushy({ appKey: 'demo-app' }); + const errors: any[] = []; + client.onError((e: any) => { + errors.push(e); + }); + + const results = await Promise.allSettled([ + client.markSuccess(), + client.markSuccess(), + ]); + expect(results.map((r) => r.status)).toEqual(['rejected', 'rejected']); + // One failure, one report — not one per caller. + expect(errors).toHaveLength(1); + expect(sharedState.marked).toBe(false); + + await client.markSuccess(); + expect(nativeMarkSuccess).toHaveBeenCalledTimes(2); + expect(sharedState.marked).toBe(true); + }); + test('waits for beforeReload before switching version', async () => { const calls: string[] = []; const reloadUpdate = mock(() => { @@ -1002,6 +1058,7 @@ describe('downloadUpdate fallback chain', () => { addProgressListener = mock(() => ({ remove: mock(() => {}) })), setLocalHashInfo = mock(() => {}), fetchWithTimeout = mock(() => Promise.resolve()), + testUrls = mock((urls?: string[]) => Promise.resolve(urls?.[0] || null)), }: { downloadPatchFromPpk?: ReturnType; downloadPatchFromPackage?: ReturnType; @@ -1009,6 +1066,7 @@ describe('downloadUpdate fallback chain', () => { addProgressListener?: ReturnType; setLocalHashInfo?: ReturnType; fetchWithTimeout?: ReturnType; + testUrls?: ReturnType; } = {}) => { setupClientMocks({ downloadPatchFromPpk, @@ -1038,10 +1096,11 @@ describe('downloadUpdate fallback chain', () => { log: mock(() => {}), noop: () => {}, promiseAny: mock(() => Promise.resolve()), - testUrls: (urls?: string[]) => Promise.resolve(urls?.[0] || null), + testUrls, })); return { + testUrls, downloadPatchFromPpk, downloadPatchFromPackage, downloadFullUpdate, @@ -1353,6 +1412,35 @@ describe('downloadUpdate fallback chain', () => { expect(downloadFullUpdate).toHaveBeenCalledTimes(2); }); + test('mirrors are ranked once per strategy across retries; a failed winner is demoted', async () => { + const fullUrls: string[] = []; + let fullCalls = 0; + const { testUrls } = setupDownloadMocks({ + downloadPatchFromPpk: mock(() => Promise.reject(Error('diff fail'))), + downloadPatchFromPackage: mock(() => Promise.reject(Error('pdiff fail'))), + downloadFullUpdate: mock(({ updateUrl }: { updateUrl: string }) => { + fullUrls.push(updateUrl); + // Attempt 0 fails on both mirrors; attempt 1 succeeds on its first. + return ++fullCalls <= 2 + ? Promise.reject(Error('full fail')) + : Promise.resolve(); + }), + }); + const { Pushy, sharedState } = await importFreshClient('dl-rank-once'); + sharedState.downloadedHash = undefined; + const client = new Pushy({ appKey: 'demo-app', maxRetries: 3 }); + + expect(await client.downloadUpdate(twoMirrors)).toBe('new-hash'); + // diff, pdiff, full: one HEAD race each, not one per attempt. + expect(testUrls).toHaveBeenCalledTimes(3); + expect(fullUrls).toEqual([ + 'https://cdn-a.example.com/full.ppk', + 'https://cdn-b.example.com/full.ppk', + // The HEAD winner failed: the retry starts from the other mirror. + 'https://cdn-b.example.com/full.ppk', + ]); + }); + test('defaults to 3 retries when maxRetries is not set', async () => { const { downloadFullUpdate } = setupDownloadMocks({ downloadPatchFromPpk: mock(() => Promise.reject(Error('diff fail'))), @@ -2165,6 +2253,71 @@ describe('syncNativeConfig', () => { expect(syncNativeConfig).toHaveBeenCalledTimes(2); }); + test('switching ownership to native and back writes the config again', async () => { + const syncNativeConfig = mock(() => Promise.resolve()); + setupClientMocks({ syncNativeConfig }); + const { Pushy } = await importFreshClient('sync-config-owner-roundtrip'); + const client = new Pushy({ appKey: 'demo-app' }); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + expect(syncNativeConfig).toHaveBeenCalledTimes(1); + + // While native owns it, the host may have stored anything: the old JS + // value says nothing about native storage any more. + client.setOptions({ nativeConfigSource: 'native' }); + client.setOptions({ nativeConfigSource: 'javascript' }); + + expect(syncNativeConfig).toHaveBeenCalledTimes(2); + expect(syncNativeConfig.mock.calls[1]).toEqual( + syncNativeConfig.mock.calls[0] + ); + }); + + test('a host-owned native config (NATIVE_CONFIG_CONFLICT) fails loudly and stops further writes', async () => { + const syncNativeConfig = mock(() => + Promise.reject( + Object.assign(new Error('configure() was called'), { + code: 'NATIVE_CONFIG_CONFLICT', + }) + ) + ); + setupClientMocks({ syncNativeConfig }); + const consoleError = spyOn(console, 'error').mockImplementation(() => {}); + const logger = mock(() => {}); + const { Pushy } = await importFreshClient('sync-config-conflict'); + const client = new Pushy({ appKey: 'demo-app', logger }); + const errors: Array<{ e: any; eventType: string }> = []; + client.onError((e: any, eventType: string) => { + errors.push({ e, eventType }); + }); + await Promise.resolve(); + await Promise.resolve(); + await Promise.resolve(); + + expect(errors).toHaveLength(1); + expect(errors[0].e.code).toBe('NATIVE_CONFIG_CONFLICT'); + expect(errors[0].eventType).toBe('errorNativeConfig'); + expect(consoleError).toHaveBeenCalledWith( + expect.any(String), + expect.stringContaining('error_native_config_conflict') + ); + await Promise.resolve(); + expect(logger).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'errorNativeConfig', + data: expect.objectContaining({ code: 'NATIVE_CONFIG_CONFLICT' }), + }) + ); + + // Retrying would only repeat the same rejection. + client.setOptions({ updateStrategy: 'silentAndNow' }); + await Promise.resolve(); + expect(syncNativeConfig).toHaveBeenCalledTimes(1); + expect(errors).toHaveLength(1); + consoleError.mockRestore(); + }); + test('an older native module without the method is skipped silently', async () => { setupClientMocks(); const { Pushy } = await importFreshClient('sync-config-4'); @@ -2368,6 +2521,132 @@ describe('native check cache reuse', () => { expect(await client.checkUpdate()).toEqual(networkResult); }); + test('an expired cache is judged on its age alone and never read again', async () => { + const getNativeCheckCache = mock(() => + Promise.resolve( + JSON.stringify({ + ts: Math.floor(Date.now() / 1000) - 600, + body: JSON.stringify({ upToDate: true }), + // Never parsed: the age check comes first. + request: '{not json', + config: '{not json', + }) + ) + ); + const syncNativeConfig = mock(() => Promise.resolve()); + setupClientMocks({ getNativeCheckCache, syncNativeConfig }); + const networkResult = { update: true, hash: 'net-hash' }; + (globalThis as any).fetch = mock(async () => + createJsonResponse(networkResult) + ); + const { Pushy } = await importFreshClient('native-cache-expired-once'); + const client = new Pushy({ appKey: 'demo-app' }); + + const parse = spyOn(JSON, 'parse'); + let parsedCacheFields: unknown[][] = []; + try { + expect(await client.checkUpdate()).toEqual(networkResult); + parsedCacheFields = parse.mock.calls.filter( + ([text]) => text === '{not json' + ); + } finally { + parse.mockRestore(); + } + expect(parsedCacheFields).toHaveLength(0); + // A different request (outside the dedup window's identity) re-checks, + // but the native cache is not consulted again in this process. + expect(await client.checkUpdate({ toHash: 'other' })).toEqual( + networkResult + ); + expect(getNativeCheckCache).toHaveBeenCalledTimes(1); + }); + + test('an absent cache is not read again in the same process', async () => { + const getNativeCheckCache = mock(() => Promise.resolve(null)); + const syncNativeConfig = mock(() => Promise.resolve()); + setupClientMocks({ getNativeCheckCache, syncNativeConfig }); + const networkResult = { upToDate: true }; + (globalThis as any).fetch = mock(async () => + createJsonResponse(networkResult) + ); + const { Pushy } = await importFreshClient('native-cache-absent-once'); + const client = new Pushy({ appKey: 'demo-app' }); + + await client.checkUpdate(); + await client.checkUpdate({ toHash: 'other' }); + + expect(getNativeCheckCache).toHaveBeenCalledTimes(1); + }); + + test('a config that is canonically equal (C1) still matches the cache', async () => { + const cachedResult = { upToDate: true }; + let configJson = ''; + const syncNativeConfig = mock((value: string) => { + configJson = value; + return Promise.resolve(); + }); + const getNativeCheckCache = mock(() => { + // What a native normalizer might have stored: trailing slashes, a + // duplicate endpoint, default-valued keys spelled out, other key order. + const { endpoints, ...rest } = JSON.parse(configJson); + const nativeConfig = { + disabled: false, + endpoints: [...endpoints.map((url: string) => `${url}/`), endpoints[0]], + ...rest, + }; + return Promise.resolve( + JSON.stringify({ + ts: Math.floor(Date.now() / 1000) - 10, + body: JSON.stringify(cachedResult), + request: expectedRequestBody, + config: JSON.stringify(nativeConfig), + }) + ); + }); + setupClientMocks({ getNativeCheckCache, syncNativeConfig }); + const { Pushy } = await importFreshClient('native-cache-canonical-config'); + const client = new Pushy({ appKey: 'demo-app' }); + + expect(await client.checkUpdate()).toEqual(cachedResult); + }); + + test('markJsCheckCompleted is sent once per config after it succeeds', async () => { + const syncNativeConfig = mock(() => Promise.resolve()); + let markCalls = 0; + const markJsCheckCompleted = mock(() => + ++markCalls === 1 ? Promise.reject(Error('busy')) : Promise.resolve() + ); + setupClientMocks({ syncNativeConfig, markJsCheckCompleted }); + (globalThis as any).fetch = mock(async () => + createJsonResponse({ upToDate: true }) + ); + const { Pushy } = await importFreshClient('mark-js-check-once'); + const client = new Pushy({ appKey: 'demo-app' }); + const flushAll = async () => { + for (let i = 0; i < 5; i++) { + await Promise.resolve(); + } + }; + + // The first acknowledgement failed: the next completed check resends. + await client.checkUpdate({ round: 1 }); + await flushAll(); + await client.checkUpdate({ round: 2 }); + await flushAll(); + expect(markJsCheckCompleted).toHaveBeenCalledTimes(2); + + // Acknowledged for this config: further checks stay quiet... + await client.checkUpdate({ round: 3 }); + await flushAll(); + expect(markJsCheckCompleted).toHaveBeenCalledTimes(2); + + // ...until the config changes. + client.setOptions({ updateStrategy: 'silentAndNow' }); + await client.checkUpdate({ round: 4 }); + await flushAll(); + expect(markJsCheckCompleted).toHaveBeenCalledTimes(3); + }); + test('a cache timestamp from the future is never reused', async () => { let configJson = ''; const syncNativeConfig = mock((value: string) => { diff --git a/src/__tests__/core.test.ts b/src/__tests__/core.test.ts index 0a09dd1e..e9489624 100644 --- a/src/__tests__/core.test.ts +++ b/src/__tests__/core.test.ts @@ -46,10 +46,6 @@ describe('core info parsing', () => { version: { major: 0, minor: 73, patch: 0 }, })); - mock.module('nanoid/non-secure', () => ({ - nanoid: () => 'mock-uuid', - })); - // Use a unique query parameter to bypass cache if supported, or just rely on fresh environment per file. // In Bun, you can sometimes use a cache buster if it's dynamic import. await importFreshCore('error'); @@ -178,9 +174,6 @@ describe('web platform', () => { mock.module('react-native/Libraries/Core/ReactNativeVersion', () => ({ version: { major: 0, minor: 73, patch: 0 }, })); - mock.module('nanoid/non-secure', () => ({ - nanoid: () => 'web-uuid', - })); const consoleError = spyOn(console, 'error').mockImplementation(() => {}); const consoleWarn = spyOn(console, 'warn').mockImplementation(() => {}); @@ -193,8 +186,7 @@ describe('web platform', () => { expect(typeof core.currentVersion).toBe('string'); expect(typeof core.packageVersion).toBe('string'); expect(typeof core.buildTime).toBe('string'); - expect(typeof core.cInfo.uuid).toBe('string'); - expect(core.cInfo.uuid.length).toBeGreaterThan(0); + expect(core.cInfo.uuid).toMatch(/^[A-Za-z0-9_-]{21}$/); expect(core.currentVersionInfo).toEqual({}); expect(core.supportedDiffVersion).toBe(0); expect(core.getBundleHash()).toBe(''); diff --git a/src/__tests__/provider.render.test.tsx b/src/__tests__/provider.render.test.tsx index d8e1f502..eea8efcb 100644 --- a/src/__tests__/provider.render.test.tsx +++ b/src/__tests__/provider.render.test.tsx @@ -660,4 +660,36 @@ describe('UpdateProvider rendering', () => { ); expect(mockAlert).not.toHaveBeenCalled(); }); + test('the launch deep link is handled once, even when the strategy callbacks change', async () => { + const { Linking } = await import('react-native'); + const getInitialURL = mock(() => + Promise.resolve('app://open?type=__rnPushyVersionHash&data=test-hash') + ); + (Linking as any).getInitialURL = getInitialURL; + const client = createClient({ + updateStrategy: 'alwaysAlert', + checkStrategy: null, + }); + client.checkUpdate.mockImplementation(async () => ({ upToDate: true })); + await renderProvider(client); + + const testChecks = () => + client.checkUpdate.mock.calls.filter( + (call: any[]) => call[0]?.toHash === 'test-hash' + ).length; + expect(getInitialURL).toHaveBeenCalledTimes(1); + expect(testChecks()).toBe(1); + + // A strategy change rebuilds checkUpdate and with it parseTestPayload; + // the Linking effect used to re-run and replay the initial URL. + for (const updateStrategy of ['silentAndNow', 'silentAndLater']) { + await TestRenderer.act(async () => { + client.setOptions({ updateStrategy }); + await flush(); + }); + } + + expect(getInitialURL).toHaveBeenCalledTimes(1); + expect(testChecks()).toBe(1); + }); }); diff --git a/src/__tests__/updateFlowCore.test.ts b/src/__tests__/updateFlowCore.test.ts index 8c7343da..331d5c93 100644 --- a/src/__tests__/updateFlowCore.test.ts +++ b/src/__tests__/updateFlowCore.test.ts @@ -3,9 +3,11 @@ import type { CheckResult } from '../type'; import { buildCheckFingerprint, buildCheckRequestBody, + canonicalizeNativeConfig, decideDownload, isMirrorRetryableCode, isValidCheckResult, + nativeConfigsEquivalent, orderEndpointCandidates, } from '../updateFlowCore'; @@ -335,3 +337,87 @@ describe('isValidCheckResult', () => { expect(isValidCheckResult({ update: 1 })).toBe(false); }); }); + +describe('native config canonical equality (C1)', () => { + const base = { + appKey: 'demo-app', + packageVersion: '1.0.0', + endpoints: ['https://a.example.com', 'https://b.example.com'], + queryUrls: ['https://q.example.com/endpoints.json'], + afterDownload: 'setNeedUpdate', + rnu: '10.0.0', + rn: '0.73.0', + }; + const json = (value: unknown) => JSON.stringify(value); + + test('key order is ignored', () => { + const reordered = Object.fromEntries(Object.entries(base).reverse()); + expect(nativeConfigsEquivalent(json(base), json(reordered))).toBe(true); + }); + + test('absent keys equal their native defaults', () => { + const minimal = { + appKey: 'demo-app', + endpoints: ['https://a.example.com'], + }; + const explicit = { + ...minimal, + disabled: false, + queryUrls: [], + rnu: '', + rn: '', + afterDownload: 'none', + }; + expect(nativeConfigsEquivalent(json(minimal), json(explicit))).toBe(true); + expect( + nativeConfigsEquivalent( + json(minimal), + json({ ...minimal, afterDownload: 'setNeedUpdate' }) + ) + ).toBe(false); + }); + + test('endpoint lists ignore trailing slashes and duplicates, not order', () => { + const noisy = { + ...base, + endpoints: [ + 'https://a.example.com/', + 'https://a.example.com', + 'https://b.example.com//', + ], + queryUrls: [ + 'https://q.example.com/endpoints.json/', + 'https://q.example.com/endpoints.json', + ], + }; + expect(nativeConfigsEquivalent(json(base), json(noisy))).toBe(true); + expect(canonicalizeNativeConfig(json(noisy))?.endpoints).toEqual([ + 'https://a.example.com', + 'https://b.example.com', + ]); + const swapped = { + ...base, + endpoints: ['https://b.example.com', 'https://a.example.com'], + }; + expect(nativeConfigsEquivalent(json(base), json(swapped))).toBe(false); + }); + + test('a real difference is detected', () => { + expect( + nativeConfigsEquivalent( + json(base), + json({ ...base, appKey: 'another-app' }) + ) + ).toBe(false); + expect( + nativeConfigsEquivalent(json(base), json({ ...base, extra: 1 })) + ).toBe(false); + }); + + test('unparseable or non-object JSON is never equivalent', () => { + expect(nativeConfigsEquivalent('not json', 'not json')).toBe(false); + expect(nativeConfigsEquivalent('null', 'null')).toBe(false); + expect(nativeConfigsEquivalent('[]', '[]')).toBe(false); + expect(nativeConfigsEquivalent(json(base), '')).toBe(false); + }); +}); diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index 828cadb2..542ee78b 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -18,6 +18,7 @@ mock.module('../i18n', () => { import { computeProgress, + createNonSecureId, enhancedFetch, fetchWithTimeout, info, @@ -440,3 +441,39 @@ describe('log gating (1.11)', () => { } }); }); + +describe('createNonSecureId', () => { + test('matches the nanoid default format (21 URL-safe chars)', () => { + const ids = new Set(); + for (let i = 0; i < 50; i++) { + const id = createNonSecureId(); + expect(id).toMatch(/^[A-Za-z0-9_-]{21}$/); + ids.add(id); + } + expect(ids.size).toBe(50); + expect(createNonSecureId(8)).toHaveLength(8); + }); +}); + +describe('testUrls on web', () => { + test('resolves the first url without probing or throwing', async () => { + mock.module('react-native', () => ({ Platform: { OS: 'web' } })); + const fetchMock = mock(async () => { + throw new Error('must not probe on web'); + }); + (globalThis as any).fetch = fetchMock; + const cacheKey = 'web-ping'; + const webUtils: typeof import('../utils') = await import( + `../utils?${cacheKey}` + ); + + expect(webUtils.isWeb).toBe(true); + expect( + await webUtils.testUrls([ + 'https://a.example.com', + 'https://b.example.com', + ]) + ).toBe('https://a.example.com'); + expect(fetchMock).not.toHaveBeenCalled(); + }); +}); diff --git a/src/client.ts b/src/client.ts index eb71cd49..7c864bdd 100644 --- a/src/client.ts +++ b/src/client.ts @@ -55,6 +55,7 @@ import { decideDownload, isMirrorRetryableCode, isValidCheckResult, + nativeConfigsEquivalent, } from './updateFlowCore'; import { assertWeb, @@ -63,6 +64,7 @@ import { fetchWithTimeout, info, log, + error as logError, noop, promiseAny, setDebugLogging, @@ -157,6 +159,17 @@ export const sharedState: { applyingUpdate: false, }; +// The native cold-start check writes its response cache at most once per +// launch (and skips it once JS has checked), so once this process has seen it +// absent or expired, later reads are near-certain misses: skip the bridge +// call from then on. The rare miss costs one network check. +let nativeCheckCacheExhausted = false; +const NATIVE_CHECK_CACHE_MAX_AGE_S = 120; + +// In-flight native markSuccess shared by concurrent callers, so the native +// call and its telemetry happen once. Cleared once settled. +let markingPromise: Promise | undefined; + // A reset/new switch invalidates ownership of older async completions and // watchdogs before the process-wide applyingUpdate flag can be reused. let applyingUpdateGeneration = 0; @@ -242,6 +255,11 @@ export class Pushy { private syncedNativeConfigJson?: string; private pendingNativeConfigJson?: string; private nativeConfigSyncInFlight = false; + // Set once native rejected a write with NATIVE_CONFIG_CONFLICT: the host + // owns the config, so further writes would only repeat the same error. + private nativeConfigConflict = false; + // Config last acknowledged by markJsCheckCompleted. + private jsCheckCompletedConfigJson?: string; private reportedInvalidUpdates = new Set(); private reportedExceptions = new WeakSet(); private globalErrorCleanup?: () => void; @@ -422,9 +440,10 @@ export class Pushy { private flushNativeConfig = () => { if (this.options.nativeConfigSource === 'native') { this.pendingNativeConfigJson = undefined; + this.syncedNativeConfigJson = undefined; return; } - if (this.nativeConfigSyncInFlight) { + if (this.nativeConfigSyncInFlight || this.nativeConfigConflict) { return; } const configJson = this.pendingNativeConfigJson; @@ -443,9 +462,18 @@ export class Pushy { } syncResult .then(() => { - this.syncedNativeConfigJson = configJson; + // A switch to 'native' while this write was in flight hands storage + // to the host; remembering the write would skip it after switching + // back. + if (this.options.nativeConfigSource !== 'native') { + this.syncedNativeConfigJson = configJson; + } }) .catch((e: any) => { + if (readErrorCode(e).code === 'NATIVE_CONFIG_CONFLICT') { + this.handleNativeConfigConflict(e); + return; + } log('syncNativeConfig failed:', e?.message || e); }) .finally(() => { @@ -454,6 +482,24 @@ export class Pushy { }); }; + /** + * The host app called the native configure() API in this process, so it + * owns the persisted config, yet JS is in the default 'javascript' mode and + * tried to overwrite it. That is an integration error: say so loudly and + * route it through the error pipeline instead of silently dropping writes. + */ + private handleNativeConfigConflict = (e: unknown) => { + this.nativeConfigConflict = true; + this.pendingNativeConfigJson = undefined; + const err = new UpdateError( + this.t('error_native_config_conflict'), + 'NATIVE_CONFIG_CONFLICT', + { cause: e } + ); + logError(err.message); + this.emitError(err, 'errorNativeConfig'); + }; + /** * Tell the native cold-start orchestrator that this process already holds a * valid check response for the current config, so its delayed round (5s @@ -469,11 +515,14 @@ export class Pushy { return; } const configJson = this.getNativeConfigJson(); - if (!configJson) { + if (!configJson || configJson === this.jsCheckCompletedConfigJson) { return; } try { - Promise.resolve(PushyModule.markJsCheckCompleted(configJson)).catch( + Promise.resolve(PushyModule.markJsCheckCompleted(configJson)).then( + () => { + this.jsCheckCompletedConfigJson = configJson; + }, (e: any) => { log('markJsCheckCompleted failed:', e?.message || e); } @@ -485,7 +534,11 @@ export class Pushy { private syncNativeConfig = () => { if (this.options.nativeConfigSource === 'native') { + // The host owns the stored config now; forget what JS last wrote so a + // switch back to 'javascript' writes again instead of deduping + // against a value native storage no longer holds. this.pendingNativeConfigJson = undefined; + this.syncedNativeConfigJson = undefined; return; } if ( @@ -1044,34 +1097,35 @@ export class Pushy { requestBody: Record ): Promise => { try { - if (__DEV__ || typeof PushyModule.getNativeCheckCache !== 'function') { + if ( + __DEV__ || + nativeCheckCacheExhausted || + typeof PushyModule.getNativeCheckCache !== 'function' + ) { return undefined; } const raw = await Promise.resolve(PushyModule.getNativeCheckCache()); if (!raw || typeof raw !== 'string') { + nativeCheckCacheExhausted = true; return undefined; } const entry = JSON.parse(raw); - const config = this.getNativeConfig(); - const cachedRequest = - typeof entry?.request === 'string' - ? JSON.parse(entry.request) - : undefined; - const cachedConfig = - typeof entry?.config === 'string' - ? JSON.parse(entry.config) - : undefined; - if ( - typeof entry?.ts !== 'number' || - typeof entry?.body !== 'string' || - !this.jsonValuesEqual(cachedRequest, requestBody) || - !config || - !this.jsonValuesEqual(cachedConfig, config) - ) { + // Age first: it is the common miss and needs no further parsing. + const ageSeconds = + typeof entry?.ts === 'number' ? Date.now() / 1000 - entry.ts : NaN; + if (!(ageSeconds >= 0 && ageSeconds <= NATIVE_CHECK_CACHE_MAX_AGE_S)) { + nativeCheckCacheExhausted = true; return undefined; } - const ageSeconds = Date.now() / 1000 - entry.ts; - if (ageSeconds < 0 || ageSeconds > 120) { + const configJson = this.getNativeConfigJson(); + if ( + typeof entry.body !== 'string' || + typeof entry.request !== 'string' || + typeof entry.config !== 'string' || + !configJson || + !nativeConfigsEquivalent(entry.config, configJson) || + !this.jsonValuesEqual(JSON.parse(entry.request), requestBody) + ) { return undefined; } const result: unknown = JSON.parse(entry.body); @@ -1098,6 +1152,19 @@ export class Pushy { if (sharedState.marked || __DEV__ || !isFirstTime) { return; } + // Concurrent callers (auto mark + an explicit call) would all pass the + // check above before the first native call settles: share it. + if (!markingPromise) { + const promise = this.performMarkSuccess().finally(() => { + if (markingPromise === promise) { + markingPromise = undefined; + } + }); + markingPromise = promise; + } + return markingPromise; + }; + private performMarkSuccess = async () => { try { await Promise.resolve(PushyModule.markSuccess()); } catch (e) { @@ -1519,6 +1586,11 @@ export class Pushy { hash, }), }; + // Mirror ranking per strategy, kept across retry attempts: the HEAD race + // runs once per strategy per download instead of once per attempt. A + // winner that failed its download is demoted to the back for the next + // attempt (every mirror is tried on every attempt anyway). + const rankedUrls = new Map(); const errorKeys = { diff: 'error_diff_failed', pdiff: 'error_pdiff_failed', @@ -1559,7 +1631,11 @@ export class Pushy { // a transfer can die mid-stream). Every mirror of the same artifact // gets a real download attempt before the strategy is abandoned — // the native cold-start path already behaves this way. - const orderedUrls = await this.rankDownloadUrls(urls); + let orderedUrls = rankedUrls.get(type); + if (!orderedUrls) { + orderedUrls = await this.rankDownloadUrls(urls); + rankedUrls.set(type, orderedUrls); + } for (const url of orderedUrls) { log(`downloading ${type} from ${url}`); try { @@ -1573,6 +1649,9 @@ export class Pushy { // `[CODE] ` message prefix. Either way it survives into the // i18n-wrapped error together with the original as cause. const { code, message } = readErrorCode(e); + if (url === orderedUrls[0] && orderedUrls.length > 1) { + rankedUrls.set(type, [...orderedUrls.slice(1), url]); + } const errorMessage = this.t(errorKeys[type], { message: message || String(e ?? ''), }); diff --git a/src/core.ts b/src/core.ts index 15508260..89ab5ed0 100644 --- a/src/core.ts +++ b/src/core.ts @@ -1,7 +1,7 @@ import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; import { UpdateError } from './error'; import i18n from './i18n'; -import { emptyModule, error, log } from './utils'; +import { createNonSecureId, emptyModule, error, log } from './utils'; // RN's JS global object. Declared locally because RN 0.87's own typings no // longer define `global` (only @types/node did), and this source is @@ -156,7 +156,7 @@ export async function getCurrentVersionInfo(): Promise<{ export const pushyNativeEventEmitter = new NativeEventEmitter(PushyModule); if (!uuid) { - uuid = require('nanoid/non-secure').nanoid(); + uuid = createNonSecureId(); // If persisting fails the uuid drifts on every launch, which skews gray // release bucketing and inflates stats — log it instead of failing silently. // Web has nowhere to persist it (nothing runs there anyway). diff --git a/src/error.ts b/src/error.ts index df9b6e9f..10a62fb6 100644 --- a/src/error.ts +++ b/src/error.ts @@ -36,7 +36,10 @@ export type UpdateErrorCode = | 'INVALID_HASH_INFO' | 'UNSUPPORTED_PLATFORM' | 'APK_INSTALL_PERMISSION_REQUIRED' - | 'APK_INSTALL_FAILED'; + | 'APK_INSTALL_FAILED' + // syncNativeConfig refused: the host called the native configure() API in + // this process, but JS still runs in nativeConfigSource: 'javascript'. + | 'NATIVE_CONFIG_CONFLICT'; const KNOWN_CODES = new Set([ 'MODULE_NOT_LOADED', @@ -61,6 +64,7 @@ const KNOWN_CODES = new Set([ 'UNSUPPORTED_PLATFORM', 'APK_INSTALL_PERMISSION_REQUIRED', 'APK_INSTALL_FAILED', + 'NATIVE_CONFIG_CONFLICT', ]); /** diff --git a/src/locales/en.ts b/src/locales/en.ts index 502b50a7..e9fb9da5 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -50,6 +50,8 @@ export default { error_apk_download_failed: 'Failed to download or install the APK', error_reset_not_supported: 'resetToPackagedBundle is not available in the installed native module. Please rebuild the app with the latest react-native-update.', + error_native_config_conflict: + 'The native configure() API was called in this app, so the native side owns the update configuration, but the JS client is in its default nativeConfigSource: "javascript" mode. Set nativeConfigSource: "native" in the client options (and keep appKey/server consistent with the native configuration).', // Development messages dev_debug_disabled: diff --git a/src/locales/zh.ts b/src/locales/zh.ts index c11f6211..18acd20a 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -45,6 +45,8 @@ export default { error_apk_download_failed: '安装包下载或安装失败', error_reset_not_supported: '当前安装的原生模块不支持 resetToPackagedBundle,请使用最新版 react-native-update 重新打包后再试。', + error_native_config_conflict: + '应用已调用原生 configure() 接口,更新配置归原生侧所有,但 JS 客户端仍处于默认的 nativeConfigSource: "javascript" 模式。请在客户端选项中设置 nativeConfigSource: "native"(并保持 appKey/server 与原生配置一致)。', // Development messages dev_debug_disabled: diff --git a/src/provider.tsx b/src/provider.tsx index 3f531dfb..b8163a79 100644 --- a/src/provider.tsx +++ b/src/provider.tsx @@ -539,6 +539,16 @@ export const UpdateProvider = ({ [client] ); + // parseTestPayload's identity follows checkUpdate (and through it the + // update strategy callbacks); the Linking subscription reads it through a + // ref so it is set up once instead of re-running — and replaying the + // initial URL — on every such change. + const latestParseTestPayload = useRef(parseTestPayload); + useEffect(() => { + latestParseTestPayload.current = parseTestPayload; + }, [parseTestPayload]); + const initialUrlHandled = useRef(false); + useEffect(() => { if (!assertWeb()) { return; @@ -552,13 +562,18 @@ export const UpdateProvider = ({ // malformed deep link must not become an unhandled rejection / a throw // inside the 'url' event handler. const params = parseQueryParams(url); - parseTestPayload({ + latestParseTestPayload.current({ type: params.type ?? null, data: params.data ?? null, }); }; - Linking.getInitialURL().then(parseLinking).catch(noop); + // The launch URL is one event: a remount (StrictMode included) must not + // replay it. + if (!initialUrlHandled.current) { + initialUrlHandled.current = true; + Linking.getInitialURL().then(parseLinking).catch(noop); + } const linkingHandler = ({ url }: { url: string }) => { parseLinking(url); }; @@ -570,7 +585,7 @@ export const UpdateProvider = ({ linkingListener.remove(); } }; - }, [parseTestPayload]); + }, []); // progress lives in its own context (see context.ts), so this value only // changes when the update state itself changes, not on every progress tick. diff --git a/src/type.ts b/src/type.ts index 6931c273..3b4ffe5a 100644 --- a/src/type.ts +++ b/src/type.ts @@ -93,6 +93,9 @@ export type EventType = | 'crashRescue' | 'reset' | 'errorReset' + // The host app configured the native module itself while JS still claims + // ownership of the native config (NATIVE_CONFIG_CONFLICT). Local only. + | 'errorNativeConfig' // Server reported bundleStatus: unknownBundle — the installed binary is not // registered on the update platform (developer-facing only, local logger; // not a server telemetry event). diff --git a/src/updateFlowCore.ts b/src/updateFlowCore.ts index ce39ba4a..482d3073 100644 --- a/src/updateFlowCore.ts +++ b/src/updateFlowCore.ts @@ -118,6 +118,105 @@ export const dedupeEndpoints = ( return result; }; +// Values the natives assume for keys a persisted native check config omits. +const NATIVE_CONFIG_DEFAULTS: Record = { + disabled: false, + queryUrls: [], + rnu: '', + rn: '', + afterDownload: 'none', +}; + +const canonicalUrlList = (value: unknown): unknown => { + if (!Array.isArray(value)) { + return value; + } + const result: unknown[] = []; + const visited = new Set(); + for (const item of value) { + if (typeof item !== 'string') { + result.push(item); + continue; + } + const url = item.replace(/\/+$/, ''); + if (!visited.has(url)) { + visited.add(url); + result.push(url); + } + } + return result; +}; + +/** + * Canonical form of a persisted native check config (shared contract C1 with + * the Android/iOS/Harmony natives): absent keys take their native defaults, + * `endpoints`/`queryUrls` lose trailing '/' and duplicates (first occurrence + * kept). Undefined when the JSON is unparseable or not an object. + */ +export function canonicalizeNativeConfig( + configJson: string +): Record | undefined { + let parsed: unknown; + try { + parsed = JSON.parse(configJson); + } catch { + return undefined; + } + if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { + return undefined; + } + const canonical: Record = { + ...NATIVE_CONFIG_DEFAULTS, + ...(parsed as Record), + }; + for (const key of ['endpoints', 'queryUrls']) { + if (key in canonical) { + canonical[key] = canonicalUrlList(canonical[key]); + } + } + return canonical; +} + +const jsonDeepEqual = (a: unknown, b: unknown): boolean => { + if (a === b) { + return true; + } + if (Array.isArray(a) || Array.isArray(b)) { + return ( + Array.isArray(a) && + Array.isArray(b) && + a.length === b.length && + a.every((value, index) => jsonDeepEqual(value, b[index])) + ); + } + if (!a || !b || typeof a !== 'object' || typeof b !== 'object') { + return false; + } + const left = a as Record; + const right = b as Record; + const leftKeys = Object.keys(left); + return ( + leftKeys.length === Object.keys(right).length && + leftKeys.every( + (key) => Object.hasOwn(right, key) && jsonDeepEqual(left[key], right[key]) + ) + ); +}; + +/** + * Whether two native check config JSON strings describe the same config + * (contract C1): their canonical forms are deep-equal, key order ignored. + * Anything unparseable is never equivalent. + */ +export function nativeConfigsEquivalent( + leftJson: string, + rightJson: string +): boolean { + const left = canonicalizeNativeConfig(leftJson); + const right = canonicalizeNativeConfig(rightJson); + return !!left && !!right && jsonDeepEqual(left, right); +} + /** * The endpoint plan: dedupe, then move the sampled pick to the front (load * spreading), keeping the rest in configured order as the fallback sequence. diff --git a/src/utils.ts b/src/utils.ts index 2dd98319..2a8302b3 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -57,6 +57,23 @@ export function promiseAny(promises: Promise[]) { }); } +// Same alphabet and length as nanoid's default (URL-safe, 21 chars), so ids +// stay format-compatible with the ones nanoid/non-secure used to generate. +const ID_ALPHABET = + 'useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict'; + +/** + * Non-cryptographic random id (installation uuid). Math.random is enough: it + * only buckets gray releases, it is not a secret. + */ +export const createNonSecureId = (size = 21): string => { + let id = ''; + for (let i = 0; i < size; i++) { + id += ID_ALPHABET[Math.floor(Math.random() * 64)]; + } + return id; +}; + export const noop = () => {}; const emptyModuleTarget: Record = {}; export const emptyModule = new Proxy(emptyModuleTarget, { @@ -65,8 +82,11 @@ export const emptyModule = new Proxy(emptyModuleTarget, { }, }); +// Web never probes (assertWeb gates every flow); the identity keeps the +// signature. Not the bare `Promise.resolve`: called unbound through map() it +// throws. const ping = isWeb - ? Promise.resolve + ? (url: string) => Promise.resolve(url) : async (url: string) => { try { const { From 6573964ea078b6a9a75f19a2b52984d1557f00dd Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:18:25 +0800 Subject: [PATCH 03/21] perf(harmony): move bundle/resource IO off the UI thread; config equivalence and conflict IO (UI thread): - SaveFile: rawfile -> sandbox copy now uses async fileIo (was openSync/ readSync/writeSync on the TurboModule thread); withRawFd serializes same-name getRawFd/closeRawFd so one close cannot pull an fd from under another reader. - pdiff: stream the embedded bundle from getRawFd into the temp origin file instead of getRawFileContent + write (which also wrote the whole underlying ArrayBuffer of the view). - bundleHash: new sha256HexRawFdAsync(fd, offset, length) in pushy.cpp (napi async work, pread loop, short read = reject); in-flight computation shared between JS and the native round. Removed the now-unused sync sha256Hex / sha256HexFile exports. - copySandboxFile: fileIo.copyFile (old loop allocated per chunk and took a short read for EOF). Archive limits: - checkOriginalSize: getOriginalSize failures still refuse the archive, now with their own detail ("archive expansion size unavailable") and log line. - planUnknownLengthFreeSpaceProbe extracted from the download write path. - Documented why the compression ratio is checked on archive totals (zlib exposes no per-entry sizes) and how that differs from archive_limits.h. Configuration: - C1: canonical config equivalence (defaults, trailing-slash strip + dedupe of endpoints/queryUrls, key order ignored); used by hasJsRound and by setNativeConfig (equivalent configs no longer bump generations or drop the response cache). - C2: syncNativeConfig rejects with NATIVE_CONFIG_CONFLICT once the host called configure() in this process (local constant until error_codes.h carries kNativeConfigConflict). - SyncCoordinator: one configuration gate for preflight, host API and round, with a consistently trimmed appKey. - Fixed the misplaced setKv doc comment. Tests: hypium suites for the probe plan, original-size refusal and C1; constant parity now asserts kUnknownLengthFreeSpaceProbeBytes is parsed. Co-Authored-By: Claude Opus 5.5 --- harmony/pushy/src/main/cpp/pushy.cpp | 215 ++++++++++++------ harmony/pushy/src/main/ets/ArchiveLimits.ts | 76 ++++++- harmony/pushy/src/main/ets/DownloadTask.ts | 147 +++++------- harmony/pushy/src/main/ets/ErrorCodes.ts | 5 + harmony/pushy/src/main/ets/NativePatchCore.ts | 11 +- .../main/ets/PushyFileJSBundleProvider.ets | 2 +- .../pushy/src/main/ets/PushyTurboModule.ts | 9 + harmony/pushy/src/main/ets/SaveFile.ts | 109 +++++++-- harmony/pushy/src/main/ets/SyncCoordinator.ts | 129 +++++------ harmony/pushy/src/main/ets/UpdateContext.ts | 158 ++++++++++++- harmony/pushy/src/test/ArchiveLimits.test.ets | 104 +++++++++ harmony/pushy/src/test/List.test.ets | 4 + harmony/pushy/src/test/NativeConfig.test.ets | 59 +++++ .../pushy/src/test/check-constant-parity.js | 8 + 14 files changed, 769 insertions(+), 267 deletions(-) create mode 100644 harmony/pushy/src/test/ArchiveLimits.test.ets create mode 100644 harmony/pushy/src/test/NativeConfig.test.ets diff --git a/harmony/pushy/src/main/cpp/pushy.cpp b/harmony/pushy/src/main/cpp/pushy.cpp index 3b5aeedf..31f35e7b 100644 --- a/harmony/pushy/src/main/cpp/pushy.cpp +++ b/harmony/pushy/src/main/cpp/pushy.cpp @@ -5,7 +5,10 @@ #include #include +#include +#include #include +#include #include #include "archive_patch_core.h" @@ -685,6 +688,69 @@ struct Sha256FileWork { std::string hex; }; +// Digest of a byte range of an fd the caller keeps open (rawfile +// descriptors: the embedded bundle lives at [offset, offset + length) inside +// the HAP). The fd is never closed here; JS closes it after the promise +// settles. +struct Sha256RangeWork { + napi_async_work work = nullptr; + napi_deferred deferred = nullptr; + int fd = -1; + int64_t offset = 0; + int64_t length = 0; + std::string hex; + std::string error; +}; + +// pread loop (positioned: never moves a file offset the caller may share). +// A short read before `length` bytes is an error, not EOF. +void HashFdRange(Sha256RangeWork* w) { + pushy::digest::Sha256 hasher; + std::vector buffer(256 * 1024); + int64_t position = w->offset; + int64_t remaining = w->length; + while (remaining > 0) { + const size_t want = static_cast( + remaining < static_cast(buffer.size()) + ? remaining + : static_cast(buffer.size())); + const ssize_t read = pread(w->fd, buffer.data(), want, static_cast(position)); + if (read < 0) { + if (errno == EINTR) { + continue; + } + w->error = "sha256HexRawFdAsync: read failed (errno " + + std::to_string(errno) + ")"; + return; + } + if (read == 0) { + w->error = "sha256HexRawFdAsync: unexpected end of file"; + return; + } + hasher.Update(buffer.data(), static_cast(read)); + position += read; + remaining -= read; + } + w->hex = hasher.HexDigest(); +} + +bool GetNonNegativeInt64( + napi_env env, + napi_value value, + const char* name, + int64_t* out_value) { + napi_valuetype type = napi_undefined; + if (value == nullptr || !GetValueType(env, value, &type) || + type != napi_number || + napi_get_value_int64(env, value, out_value) != napi_ok || + *out_value < 0) { + ThrowError(env, std::string("sha256HexRawFdAsync: ") + name + + " must be a non-negative number"); + return false; + } + return true; +} + napi_value ApplyPatchFromFileSource(napi_env env, napi_callback_info info) { napi_value args[1] = {nullptr}; size_t argc = 1; @@ -950,6 +1016,87 @@ napi_value Sha256HexFileAsync(napi_env env, napi_callback_info info) { return promise; } +// sha256HexRawFdAsync(fd, offset, length) -> Promise, hashed on a libuv +// worker thread. Used for the embedded bundle (resourceManager.getRawFd): the +// UI thread neither reads the multi-MB bundle into memory nor hashes it. +// Rejects on any read failure or short read. +napi_value Sha256HexRawFdAsync(napi_env env, napi_callback_info info) { + napi_value args[3] = {nullptr, nullptr, nullptr}; + size_t argc = 3; + if (!GetArgCount(env, info, &argc, args) || argc < 3) { + ThrowError(env, "sha256HexRawFdAsync: expected (fd, offset, length)"); + return nullptr; + } + int32_t fd = -1; + if (!GetInt32(env, args[0], &fd)) { + return nullptr; + } + if (fd < 0) { + ThrowError(env, "sha256HexRawFdAsync: fd must be non-negative"); + return nullptr; + } + int64_t offset = 0; + int64_t length = 0; + if (!GetNonNegativeInt64(env, args[1], "offset", &offset) || + !GetNonNegativeInt64(env, args[2], "length", &length)) { + return nullptr; + } + + auto* work_data = new Sha256RangeWork(); + work_data->fd = fd; + work_data->offset = offset; + work_data->length = length; + + napi_value promise = nullptr; + if (napi_create_promise(env, &work_data->deferred, &promise) != napi_ok) { + delete work_data; + ThrowError(env, "Unable to create promise"); + return nullptr; + } + + napi_value resource_name = nullptr; + napi_create_string_utf8( + env, "sha256HexRawFdAsync", NAPI_AUTO_LENGTH, &resource_name); + if (napi_create_async_work( + env, + nullptr, + resource_name, + [](napi_env, void* data) { + HashFdRange(static_cast(data)); + }, + [](napi_env cb_env, napi_status status, void* data) { + auto* w = static_cast(data); + if (status != napi_ok) { + RejectDeferredWithMessage(cb_env, w->deferred, "async work aborted"); + } else if (!w->error.empty() || w->hex.empty()) { + RejectDeferredWithMessage( + cb_env, + w->deferred, + w->error.empty() ? "sha256HexRawFdAsync: no digest" + : w->error.c_str()); + } else { + napi_resolve_deferred(cb_env, w->deferred, NewString(cb_env, w->hex)); + } + napi_delete_async_work(cb_env, w->work); + delete w; + }, + work_data, + &work_data->work) != napi_ok) { + RejectDeferredWithMessage( + env, work_data->deferred, "Unable to create async work"); + delete work_data; + return promise; + } + if (napi_queue_async_work(env, work_data->work) != napi_ok) { + napi_delete_async_work(env, work_data->work); + RejectDeferredWithMessage( + env, work_data->deferred, "Unable to queue async work"); + delete work_data; + return promise; + } + return promise; +} + bool ExportFunction( napi_env env, napi_value exports, @@ -972,71 +1119,6 @@ bool ExportFunction( } // namespace -// sha256(Uint8Array | ArrayBuffer) -> 小写 hex。同步:bundleHash 的输入是 -// rawfile 里的 bundle(已整体读入内存,与 pdiff 的读法一致),哈希本身毫秒级。 -static napi_value MakeUtf8String(napi_env env, const std::string& value); - -// Streaming file digest (bounded memory). Synchronous: small files only — -// archives and bundles go through sha256HexFileAsync. -static napi_value Sha256HexFile(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1] = {nullptr}; - if (!GetArgCount(env, info, &argc, args) || argc < 1) { - ThrowError(env, "sha256HexFile: missing path argument"); - return nullptr; - } - bool ok = false; - std::string path = GetString(env, args[0], &ok); - if (!ok) { - // GetString already raised the pending exception. - return nullptr; - } - return MakeUtf8String(env, pushy::digest::Sha256File(path)); -} - -static napi_value Sha256Hex(napi_env env, napi_callback_info info) { - size_t argc = 1; - napi_value args[1] = {nullptr}; - if (!GetArgCount(env, info, &argc, args) || argc < 1) { - ThrowError(env, "sha256Hex: missing data argument"); - return nullptr; - } - - void* data = nullptr; - size_t length = 0; - bool is_typedarray = false; - napi_is_typedarray(env, args[0], &is_typedarray); - if (is_typedarray) { - napi_typedarray_type type; - napi_value arraybuffer = nullptr; - size_t offset = 0; - if (napi_get_typedarray_info( - env, args[0], &type, &length, &data, &arraybuffer, &offset) != - napi_ok) { - ThrowError(env, "sha256Hex: cannot read typed array"); - return nullptr; - } - } else { - bool is_arraybuffer = false; - napi_is_arraybuffer(env, args[0], &is_arraybuffer); - if (!is_arraybuffer || - napi_get_arraybuffer_info(env, args[0], &data, &length) != napi_ok) { - ThrowError(env, "sha256Hex: expected Uint8Array or ArrayBuffer"); - return nullptr; - } - } - - pushy::digest::Sha256 hasher; - if (length > 0) { - hasher.Update(static_cast(data), length); - } - const std::string hex = hasher.HexDigest(); - - napi_value result = nullptr; - napi_create_string_utf8(env, hex.c_str(), hex.size(), &result); - return result; -} - // CRC32(zip/zlib 多项式)。同步:输入是已在内存的 rawfile 资源字节,用于 // pdiff 拷贝前与 __diff.json copiesCrc 比对(不符则整次 patch 失败落 full)。 static napi_value Crc32(napi_env env, napi_callback_info info) { @@ -1217,9 +1299,8 @@ napi_value Init(napi_env env, napi_value exports) { !ExportFunction(env, exports, "buildCopyGroups", BuildCopyGroups) || !ExportFunction(env, exports, "applyDeltaFromSource", ApplyPatchFromFileSource) || !ExportFunction(env, exports, "cleanupOldEntries", CleanupOldEntries) || - !ExportFunction(env, exports, "sha256Hex", Sha256Hex) || - !ExportFunction(env, exports, "sha256HexFile", Sha256HexFile) || !ExportFunction(env, exports, "sha256HexFileAsync", Sha256HexFileAsync) || + !ExportFunction(env, exports, "sha256HexRawFdAsync", Sha256HexRawFdAsync) || !ExportFunction(env, exports, "crc32", Crc32) || !ExportFunction(env, exports, "getSupportedDiffVersion", GetSupportedDiffVersion) || !ExportFunction(env, exports, "buildRequestBody", FlowBuildRequestBody) || diff --git a/harmony/pushy/src/main/ets/ArchiveLimits.ts b/harmony/pushy/src/main/ets/ArchiveLimits.ts index 29908895..44ca0674 100644 --- a/harmony/pushy/src/main/ets/ArchiveLimits.ts +++ b/harmony/pushy/src/main/ets/ArchiveLimits.ts @@ -22,10 +22,21 @@ export const UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES = 8 * 1024 * 1024; /** * 解压前的总量校验(zlib.getOriginalSize 给出的归档解压后总字节数)。 - * zlib.decompressFile 没有逐条钩子,C++ 头文件里的逐条压缩比在这里只能按 - * 整包近似:总解压量超过上限、或(总量大于 RATIO_CHECK_MIN_BYTES 时)总压缩 - * 比超过 MAX_COMPRESSION_RATIO 即拒绝——20MB 归档 100:1 的 2GB 载荷在解压前 - * 就被挡下,而不是先解出来再量。 + * + * 与 archive_limits.h 的偏差(有意为之,无法消除):头文件的压缩比上限是 + * **逐条**的(单条解压量 > RATIO_CHECK_MIN_BYTES 时才查),但鸿蒙的 + * @kit.BasicServicesKit zlib 只提供整包解压量(getOriginalSize)与整包 + * 解压(decompressFile),拿不到任何条目的压缩/解压尺寸,也没有逐条钩子。 + * 因此这里只能按整包近似:总解压量超过上限、或(总量大于 + * RATIO_CHECK_MIN_BYTES 时)总压缩比超过 MAX_COMPRESSION_RATIO 即拒绝—— + * 20MB 归档 100:1 的 2GB 载荷在解压前就被挡下,而不是先解出来再量。 + * 两个方向的差异: + * - 更松:单条高压缩比条目被同包大量不可压缩数据"稀释"时整包比可能不超限 + * (其总量仍受 MAX_TOTAL_UNCOMPRESSED_BYTES 与解压后的逐条 + * MAX_ENTRY_BYTES 约束,伤害有界); + * - 更严:大量各自 < RATIO_CHECK_MIN_BYTES 的高压缩比小文件,逐条不查, + * 但累加后整包比超限会被拒绝(真实更新包不会出现 100:1 的整包比)。 + * 解压后 measureExtractedDirectory 再逐条核对条目数/单条大小/总字节数。 */ export function checkUncompressedSize( archiveBytes: number, @@ -51,6 +62,63 @@ export function checkUncompressedSize( } } +// 解压量取不到时拒绝的专属错误细节:与"超限"拒绝区分开,日志与遥测里一眼 +// 可辨(典型原因:设备 zlib 不认该归档格式/归档损坏)。不做任何放行回退。 +export const ORIGINAL_SIZE_UNAVAILABLE = 'archive expansion size unavailable'; + +/** + * zlib.getOriginalSize 的结果校验。failure 非 undefined 表示调用本身抛错 + * (其消息);否则校验返回值必须是有限非负数。任何一种都拒绝归档:不知道 + * 展开量就放行,等于允许一个 20MB 的归档在解压后的统计跑到之前先写满磁盘。 + */ +export function checkOriginalSize( + size: number | undefined, + failure?: string, +): number { + if (failure !== undefined) { + throw createPushyError( + ERROR_DELTA_FAILED, + `${ORIGINAL_SIZE_UNAVAILABLE}: getOriginalSize failed: ${failure}`, + ); + } + if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) { + throw createPushyError( + ERROR_DELTA_FAILED, + `${ORIGINAL_SIZE_UNAVAILABLE}: getOriginalSize returned ${String(size)}`, + ); + } + return size; +} + +export interface FreeSpaceProbePlan { + /** 本次写入前是否需要探测可用空间。 */ + probe: boolean; + /** 探测时要求的字节数(不含 FREE_DISK_MARGIN_BYTES)。 */ + reserve: number; + /** 计划后的"已预留到"位置(相对本次响应体起点)。 */ + reservedUntil: number; +} + +/** + * 未知长度下载(分块/编码响应体)的磁盘探测计划:响应到达时只能预留安全 + * 余量,所以任何会写过已预留位置的写入之前都要探测,每次至少预留接下来的 + * UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES(单个 chunk 更大时按 chunk 算)—— + * 任何写入都吃不到余量。已知总长时由响应到达时的一次性检查覆盖,不探测。 + * writtenBefore 是本 chunk 之前已收到的字节数(相对本次响应体)。 + */ +export function planUnknownLengthFreeSpaceProbe( + knownTotal: number, + writtenBefore: number, + chunkBytes: number, + reservedUntil: number, +): FreeSpaceProbePlan { + const reserve = Math.max(UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES, chunkBytes); + if (knownTotal > 0 || writtenBefore + chunkBytes <= reservedUntil) { + return { probe: false, reserve, reservedUntil }; + } + return { probe: true, reserve, reservedUntil: writtenBefore + reserve }; +} + /** * 目标所在文件系统必须还能再写 bytesToWrite + 安全余量。取不到可用空间 * (返回 0/异常)视为未知,放行。 diff --git a/harmony/pushy/src/main/ets/DownloadTask.ts b/harmony/pushy/src/main/ets/DownloadTask.ts index 2bd61342..d7e960b2 100644 --- a/harmony/pushy/src/main/ets/DownloadTask.ts +++ b/harmony/pushy/src/main/ets/DownloadTask.ts @@ -4,7 +4,7 @@ import common from '@ohos.app.ability.common'; import { zlib } from '@kit.BasicServicesKit'; import { EventHub } from './EventHub'; import { DownloadTaskParams } from './DownloadTaskParams'; -import { saveFileToSandbox } from './SaveFile'; +import { saveFileToSandbox, withRawFd } from './SaveFile'; import { util } from '@kit.ArkTS'; import NativePatchCore, { ARCHIVE_PATCH_TYPE_FROM_PACKAGE, @@ -16,10 +16,11 @@ import logger from './Logger'; import { MAX_ARCHIVE_BYTES, MAX_MANIFEST_BYTES, - UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES, + checkOriginalSize, checkUncompressedSize, ensureFreeSpace, measureExtractedDirectory, + planUnknownLengthFreeSpaceProbe, } from './ArchiveLimits'; import { HARMONY_BUNDLE_FILE_NAME, @@ -127,6 +128,7 @@ function toArrayBufferSlice( const DIFF_MANIFEST_ENTRY = '__diff.json'; const TEMP_ORIGIN_BUNDLE_ENTRY = '.origin.bundle.harmony.js'; +const EMBEDDED_BUNDLE_RAWFILE = 'bundle.harmony.js'; const FILE_COPY_BUFFER_SIZE = 64 * 1024; const DOWNLOAD_CALL_TIMEOUT_MS = 10 * 60 * 1000; @@ -328,27 +330,28 @@ export class DownloadTask { /** * 解压前读取归档解压后的总字节数(zlib.getOriginalSize,API 12——RNOH 的 - * 最低 API 已覆盖)。读不到就拒绝归档:zlib.decompressFile 没有逐条钩子, - * 不知道展开量就放行等于允许一个 20MB 的归档在 measureExtractedDirectory - * 跑到之前先把磁盘写满。 + * 最低 API 已覆盖)。读不到就拒绝归档(checkOriginalSize):zlib.decompressFile + * 没有逐条钩子,不知道展开量就放行等于允许一个 20MB 的归档在 + * measureExtractedDirectory 跑到之前先把磁盘写满。拒绝带专属错误细节 + * (ORIGINAL_SIZE_UNAVAILABLE)并单独记日志,与"超限"拒绝可区分。 */ private async readOriginalSize(archiveFile: string): Promise { let size: number | undefined; + let failure: string | undefined; try { size = await zlib.getOriginalSize(archiveFile); } catch (e) { - throw createPushyError( - ERROR_DELTA_FAILED, - `cannot determine archive expansion size: ${getErrorMessage(e)}`, - ); + failure = getErrorMessage(e); } - if (typeof size !== 'number' || !Number.isFinite(size) || size < 0) { - throw createPushyError( - ERROR_DELTA_FAILED, - `cannot determine archive expansion size: ${String(size)}`, + try { + return checkOriginalSize(size, failure); + } catch (error) { + logger.error( + TAG, + `Refusing archive, expansion size unknown: ${getErrorMessage(error)}`, ); + throw error; } - return size; } /** @@ -480,8 +483,12 @@ export class DownloadTask { } - private async applyBundlePatchFromFileSource( - originContent: ArrayBuffer, + /** + * pdiff 的 bundle 补丁:hpatch 需要文件源,先把二进制内嵌的 bundle 从 + * rawfile descriptor 异步流式拷到 staging 里的临时文件(不整包读进内存、 + * 不在 UI 线程同步 IO),补丁完成后删除。 + */ + private async applyBundlePatchFromEmbeddedBundle( workingDirectory: string, bundleDeltaPath: string, outputFile: string, @@ -489,7 +496,11 @@ export class DownloadTask { ): Promise { const originBundlePath = `${workingDirectory}/${TEMP_ORIGIN_BUNDLE_ENTRY}`; try { - await this.writeFileContent(originBundlePath, originContent); + await withRawFd( + this.context.resourceManager, + EMBEDDED_BUNDLE_RAWFILE, + descriptor => saveFileToSandbox(descriptor, originBundlePath), + ); await NativePatchCore.applyDeltaFromSource({ copyFroms: [], copyTos: [], @@ -514,50 +525,14 @@ export class DownloadTask { } } + // 沙箱内文件复制(同一 rawfile 落到多个目标时,首个之外的目标从首个复制)。 + // fileIo.copyFile 在工作线程整文件复制并覆盖目标,无需逐块搬运。 private async copySandboxFile( sourceFile: string, targetFile: string, ): Promise { - let reader: fileIo.File | null = null; - let writer: fileIo.File | null = null; - const buffer = new ArrayBuffer(FILE_COPY_BUFFER_SIZE); - let offset = 0; - - try { - reader = await fileIo.open(sourceFile, fileIo.OpenMode.READ_ONLY); - await this.ensureParentDirectory(targetFile); - if (fileIo.accessSync(targetFile)) { - await fileIo.unlink(targetFile); - } - writer = await fileIo.open( - targetFile, - fileIo.OpenMode.CREATE | fileIo.OpenMode.WRITE_ONLY, - ); - - while (true) { - const readLength = await fileIo.read(reader.fd, buffer, { - offset, - length: FILE_COPY_BUFFER_SIZE, - }); - if (readLength <= 0) { - break; - } - - await fileIo.write(writer.fd, buffer.slice(0, readLength)); - offset += readLength; - - if (readLength < FILE_COPY_BUFFER_SIZE) { - break; - } - } - } finally { - if (reader) { - await fileIo.close(reader); - } - if (writer) { - await fileIo.close(writer); - } - } + await this.ensureParentDirectory(targetFile); + await fileIo.copyFile(sourceFile, targetFile); } private async downloadFile(params: DownloadTaskParams): Promise { @@ -689,19 +664,17 @@ export class DownloadTask { `archive too large: exceeded ${MAX_ARCHIVE_BYTES}`, ); } - // 未知长度:响应到达时只能预留安全余量,所以任何会写过已预留字节数的 - // 写入之前先探测,每次至少预留接下来的 PROBE 字节(单个 chunk 更大时按 - // chunk 算)——任何写入都吃不到余量(归档上限本身远大于余量)。失败保留 - // partial 供下次续传。 - const writtenBefore = received - data.byteLength; - const probeFreeSpace = totalAll <= 0 && received > freeSpaceReservedUntil; - const reserve = Math.max( - UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES, + // 未知长度:见 planUnknownLengthFreeSpaceProbe。失败保留 partial 供 + // 下次续传。 + const plan = planUnknownLengthFreeSpaceProbe( + totalAll, + received - data.byteLength, data.byteLength, + freeSpaceReservedUntil, ); - if (probeFreeSpace) { - freeSpaceReservedUntil = writtenBefore + reserve; - } + freeSpaceReservedUntil = plan.reservedUntil; + const probeFreeSpace = plan.probe; + const reserve = plan.reserve; writeQueue = writeQueue.then(async () => { if (!writer || writeError) { return; @@ -1109,12 +1082,7 @@ export class DownloadTask { if (!fileIo.accessSync(bundleDeltaPath)) { throw createPushyError(ERROR_DELTA_FAILED, 'bundle delta not found'); } - const resourceManager = this.context.resourceManager; - const originContent = await resourceManager.getRawFileContent( - 'bundle.harmony.js', - ); - await this.applyBundlePatchFromFileSource( - originContent.buffer as ArrayBuffer, + await this.applyBundlePatchFromEmbeddedBundle( work, bundleDeltaPath, `${work}/${HARMONY_BUNDLE_FILE_NAME}`, @@ -1218,28 +1186,15 @@ export class DownloadTask { continue; } - const fromContent = await resourceManager.getRawFd(currentFrom); - try { - const firstTarget = targets[0]; - const restTargets = targets.slice(1); - await this.ensureParentDirectories(targets); - if (fileIo.accessSync(firstTarget)) { - await fileIo.unlink(firstTarget); - } - saveFileToSandbox(fromContent, firstTarget); - await Promise.all( - restTargets.map(target => this.copySandboxFile(firstTarget, target)), - ); - } finally { - try { - await resourceManager.closeRawFd(currentFrom); - } catch (closeError) { - logger.error( - TAG, - `Failed to close raw fd for ${currentFrom}: ${getErrorMessage(closeError)}`, - ); - } - } + const firstTarget = targets[0]; + const restTargets = targets.slice(1); + await this.ensureParentDirectories(targets); + await withRawFd(resourceManager, currentFrom, descriptor => + saveFileToSandbox(descriptor, firstTarget), + ); + await Promise.all( + restTargets.map(target => this.copySandboxFile(firstTarget, target)), + ); } } catch (error) { const coded = toPushyError(error, ERROR_DELTA_FAILED); diff --git a/harmony/pushy/src/main/ets/ErrorCodes.ts b/harmony/pushy/src/main/ets/ErrorCodes.ts index c03f1f58..7e36cf74 100644 --- a/harmony/pushy/src/main/ets/ErrorCodes.ts +++ b/harmony/pushy/src/main/ets/ErrorCodes.ts @@ -22,6 +22,11 @@ export const ERROR_UNSUPPORTED_PLATFORM = 'UNSUPPORTED_PLATFORM'; export const ERROR_APK_INSTALL_PERMISSION_REQUIRED = 'APK_INSTALL_PERMISSION_REQUIRED'; export const ERROR_APK_INSTALL_FAILED = 'APK_INSTALL_FAILED'; +// C2:JS syncNativeConfig 撞上宿主原生 configure()。暂作本地常量(不带 +// ERROR_ 前缀,check-constant-parity 只核对 ERROR_* 与 error_codes.h): +// kNativeConfigConflict 由另一侧加入 cpp/patch_core/error_codes.h,合并时 +// 改名为 ERROR_NATIVE_CONFIG_CONFLICT 纳入镜像核对。 +export const NATIVE_CONFIG_CONFLICT_CODE = 'NATIVE_CONFIG_CONFLICT'; /** * 带稳定错误码的 Error:`code` 是属性,消息同时带 `[CODE] ` 前缀(桥丢属性时 diff --git a/harmony/pushy/src/main/ets/NativePatchCore.ts b/harmony/pushy/src/main/ets/NativePatchCore.ts index 2cba7d34..5f32d7ae 100644 --- a/harmony/pushy/src/main/ets/NativePatchCore.ts +++ b/harmony/pushy/src/main/ets/NativePatchCore.ts @@ -84,13 +84,12 @@ interface NativePatchCoreBindings { keepNames: string[], maxAgeDays: number, ): Promise; - /** sha256(小写 hex)。同步:输入是已在内存的 rawfile bundle,哈希毫秒级 */ - sha256Hex(data: Uint8Array): string; - // 文件流式 SHA-256(小写 hex;不可读时空串)。同步,仅限小文件——整包/ - // 整 bundle 用下面的异步版本,避免冻结 UI 线程。 - sha256HexFile(path: string): string; - // 同上,在 libuv 工作线程计算;文件不可读时 reject。 + // 文件流式 SHA-256(小写 hex),在 libuv 工作线程计算;文件不可读时 reject。 sha256HexFileAsync(path: string): Promise; + // fd 上 [offset, offset + length) 区间的 SHA-256(小写 hex),工作线程 + // pread 流式计算(内嵌 bundle 走 resourceManager.getRawFd)。fd 由调用方 + // 持有,promise 落定前不得关闭;读失败/提前 EOF 时 reject。 + sha256HexRawFdAsync(fd: number, offset: number, length: number): Promise; /** CRC32(zip/zlib 多项式)。pdiff 拷贝前与 copiesCrc 比对用 */ crc32(data: Uint8Array | ArrayBuffer): number; /** 原生 patch 内核可消费的 diff 轨道版本(2 = hdiffv2 轨道) */ diff --git a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets index daf03618..2e731c3e 100644 --- a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +++ b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets @@ -50,7 +50,7 @@ export class PushyFileJSBundleProvider extends JSBundleProvider { /** Configure before normal bundle resolution; no JS or network work is required. */ async configure(options: PushyConfiguration): Promise { const config = normalizePushyConfiguration(options); - await this.updateContext.setNativeConfig(config); + await this.updateContext.setHostNativeConfig(config); } /** Call after the host's real bundle resolution; never resolves it again. */ diff --git a/harmony/pushy/src/main/ets/PushyTurboModule.ts b/harmony/pushy/src/main/ets/PushyTurboModule.ts index ad73d032..b236329c 100644 --- a/harmony/pushy/src/main/ets/PushyTurboModule.ts +++ b/harmony/pushy/src/main/ets/PushyTurboModule.ts @@ -22,6 +22,7 @@ import { ERROR_RESTART_FAILED, ERROR_SWITCH_VERSION_FAILED, ERROR_UNSUPPORTED_PLATFORM, + NATIVE_CONFIG_CONFLICT_CODE, createPushyError, getErrorMessage, toPushyError, @@ -263,8 +264,16 @@ export class PushyTurboModule extends UITurboModule { // read by the orchestrator; absent config = check disabled. Validated at // write time — a corrupt config would otherwise silently disable the // native check forever with no signal. + // A host that configured natively in this process owns the configuration: + // reject loudly (C2) instead of overwriting or silently ignoring it. async syncNativeConfig(config: string): Promise { logger.debug(TAG, ',call syncNativeConfig'); + if (this.context.isHostConfigured()) { + throw createPushyError( + NATIVE_CONFIG_CONFLICT_CODE, + "host configured natively; set nativeConfigSource: 'native' in JS", + ); + } try { JSON.parse(config); } catch (e) { diff --git a/harmony/pushy/src/main/ets/SaveFile.ts b/harmony/pushy/src/main/ets/SaveFile.ts index a809162c..0b1ffdbd 100644 --- a/harmony/pushy/src/main/ets/SaveFile.ts +++ b/harmony/pushy/src/main/ets/SaveFile.ts @@ -1,34 +1,107 @@ import resourceManager from '@ohos.resourceManager'; -import fs, { ReadOptions } from '@ohos.file.fs'; +import fs from '@ohos.file.fs'; +import logger from './Logger'; +import { getErrorMessage } from './ErrorCodes'; -const COPY_BUFFER_SIZE = 64 * 1024; +const TAG = 'SaveFile'; +// 每次 read/write 都是一次工作线程往返:块大一些,几 MB 的 bundle 只需 +// 十几次往返;内存上限仍是一个块。 +const COPY_BUFFER_SIZE = 512 * 1024; -export const saveFileToSandbox = ( +// resourceManager 按文件名跟踪 rawfile descriptor(closeRawFd 只收名字): +// 同名的 getRawFd/closeRawFd 若交错,一方的 close 可能在另一方读到一半时 +// 关掉它的 fd。同名访问按到达顺序串行(不同名互不影响)。 +const rawFdChains = new Map>(); + +/** + * 打开 rawfile descriptor 交给 job,job 结束(无论成败)后关闭。同名串行。 + * descriptor 指向 HAP 内 [offset, offset + length) 区间,job 只能按 offset + * 定位读取。 + */ +export async function withRawFd( + manager: resourceManager.ResourceManager, + name: string, + job: (descriptor: resourceManager.RawFileDescriptor) => Promise, +): Promise { + const previous = rawFdChains.get(name) ?? Promise.resolve(); + let release: () => void = () => {}; + const held = new Promise(resolve => { + release = resolve; + }); + const tail = previous.then(() => held); + rawFdChains.set(name, tail); + try { + await previous; + const descriptor = await manager.getRawFd(name); + try { + return await job(descriptor); + } finally { + try { + await manager.closeRawFd(name); + } catch (closeError) { + logger.error( + TAG, + `Failed to close raw fd for ${name}: ${getErrorMessage(closeError)}`, + ); + } + } + } finally { + release(); + if (rawFdChains.get(name) === tail) { + rawFdChains.delete(name); + } + } +} + +async function writeFully( + fd: number, + buffer: ArrayBuffer, + length: number, +): Promise { + let written = 0; + while (written < length) { + const chunk = written === 0 ? buffer : buffer.slice(written, length); + const count = await fs.write(fd, chunk, { length: length - written }); + if (count <= 0) { + throw new Error(`Short write: ${written}/${length} bytes`); + } + written += count; + } +} + +/** + * 把 rawfile descriptor 指向的区间异步拷贝到沙箱文件(覆盖)。全程异步 + * fileIo:调用方在 UI 线程(TurboModule),同步 IO 会冻结界面。读到预期 + * 长度之前遇到 EOF 视为错误。 + */ +export async function saveFileToSandbox( from: resourceManager.RawFileDescriptor, toPath: string, -) => { - let to = fs.openSync(toPath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE); - - const buffer = new ArrayBuffer(COPY_BUFFER_SIZE); - let currentOffset = from.offset; +): Promise { + const to = await fs.open( + toPath, + fs.OpenMode.WRITE_ONLY | fs.OpenMode.CREATE | fs.OpenMode.TRUNC, + ); + const buffer = new ArrayBuffer( + Math.max(1, Math.min(COPY_BUFFER_SIZE, from.length)), + ); + let position = from.offset; let remaining = from.length; try { while (remaining > 0) { - const readOption: ReadOptions = { - offset: currentOffset, - length: Math.min(COPY_BUFFER_SIZE, remaining), - }; - const readLength = fs.readSync(from.fd, buffer, readOption); + const readLength = await fs.read(from.fd, buffer, { + offset: position, + length: Math.min(buffer.byteLength, remaining), + }); if (readLength <= 0) { throw new Error(`Unexpected EOF while copying resource to ${toPath}`); } - - fs.writeSync(to.fd, buffer, { length: readLength }); - currentOffset += readLength; + await writeFully(to.fd, buffer, readLength); + position += readLength; remaining -= readLength; } } finally { - fs.close(to); + await fs.close(to); } -}; +} diff --git a/harmony/pushy/src/main/ets/SyncCoordinator.ts b/harmony/pushy/src/main/ets/SyncCoordinator.ts index f812f3a2..6bbaa05d 100644 --- a/harmony/pushy/src/main/ets/SyncCoordinator.ts +++ b/harmony/pushy/src/main/ets/SyncCoordinator.ts @@ -137,28 +137,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): BundlePreparationResult | undefined { +interface ConfigRead { + json: string; + config: NativeConfig; + /** Trimmed; the same value gates the round and builds the request URL. */ + appKey: string; + /** Set when the persisted configuration cannot start a round. */ + rejection?: BundlePreparationResult; +} + +// The single configuration gate shared by the preflight, the host API and +// the round itself, so the three can never disagree about what is runnable. +function readNativeConfig(context: UpdateContext): ConfigRead { const json = context.getKv(KEY_CONFIG); + const empty: NativeConfig = {}; if (!json) { - return bundlePreparationResult('skipped', 'not_configured'); - } + return { + json: '', + config: empty, + appKey: '', + rejection: bundlePreparationResult('skipped', 'not_configured'), + }; + } + const invalid: ConfigRead = { + json, + config: empty, + appKey: '', + rejection: bundlePreparationResult('failed', 'invalid_config'), + }; + let config: NativeConfig; try { - const config = JSON.parse(json) as NativeConfig; - if (!config || typeof config !== 'object' || Array.isArray(config)) { - return bundlePreparationResult('failed', 'invalid_config'); - } - if (config.disabled) { - return bundlePreparationResult('skipped', 'disabled'); - } - if (typeof config.appKey !== 'string' || config.appKey.trim().length === 0) { - return bundlePreparationResult('failed', 'invalid_config'); - } + config = JSON.parse(json) as NativeConfig; } catch (e) { - return bundlePreparationResult('failed', 'invalid_config'); + return invalid; } - return undefined; + if (!config || typeof config !== 'object' || Array.isArray(config)) { + return invalid; + } + if (config.disabled) { + return { + json, + config, + appKey: '', + rejection: bundlePreparationResult('skipped', 'disabled'), + }; + } + const appKey = typeof config.appKey === 'string' ? config.appKey.trim() : ''; + if (!appKey) { + return invalid; + } + return { json, config, appKey }; +} + +// 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): BundlePreparationResult | undefined { + return readNativeConfig(context).rejection; } export async function prepareBundleNative( @@ -167,24 +201,11 @@ export async function prepareBundleNative( if (scheduledContext !== context) { return bundlePreparationResult('skipped', 'not_initialized'); } - const configJson = context.getKv(KEY_CONFIG); - if (!configJson) { - return bundlePreparationResult('skipped', 'not_configured'); - } - try { - const config = JSON.parse(configJson) as NativeConfig; - if (!config || typeof config !== 'object' || Array.isArray(config)) { - return bundlePreparationResult('failed', 'invalid_config'); - } - if (config.disabled) { - return bundlePreparationResult('skipped', 'disabled'); - } - if (typeof config.appKey !== 'string' || !config.appKey) { - return bundlePreparationResult('failed', 'invalid_config'); - } - } catch (e) { - return bundlePreparationResult('failed', 'invalid_config'); + const read = readNativeConfig(context); + if (read.rejection !== undefined) { + return read.rejection; } + const configJson = read.json; const result = await startNativeRound(context, scheduledRollback); if (roundConfigGeneration !== context.getNativeConfigGeneration() || configJson !== roundConfigJson || configJson !== context.getKv(KEY_CONFIG)) { @@ -205,13 +226,11 @@ export function recordJsRound(config: string): void { jsCompletedConfig = config; } -// JS 已用与原生完全相同的配置拿到有效响应(§10.3):延迟轮次就是一次重复请求。 +// JS 已用与原生等价的配置(C1:规范形式相同,而非逐字相同——宿主持有的配置 +// 与 JS 序列化格式不同)拿到有效响应(§10.3):延迟轮次就是一次重复请求。 // 只有计划内的轮次会问这个问题——JS 没起来、启动即崩、检查失败时都不会有信号。 function hasJsRound(context: UpdateContext): boolean { - return ( - jsCompletedConfig !== undefined && - jsCompletedConfig === context.getKv(KEY_CONFIG) - ); + return !!jsCompletedConfig && context.isStoredNativeConfig(jsCompletedConfig); } export function scheduleNativeSync( @@ -252,33 +271,15 @@ async function runOnce( roundGeneration = resetGeneration; roundConfigGeneration = context.getNativeConfigGeneration(); 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 = bundlePreparationResult('skipped', 'not_configured'); - return; - } - let config: NativeConfig; - try { - config = JSON.parse(configJson) as NativeConfig; - if (!config || typeof config !== 'object' || Array.isArray(config)) { - roundResult = bundlePreparationResult('failed', 'invalid_config'); - return; - } - } catch (e) { - roundResult = bundlePreparationResult('failed', 'invalid_config'); - return; - } - if (config.disabled) { - roundResult = bundlePreparationResult('skipped', 'disabled'); - return; - } - const appKey = config.appKey ?? ''; - if (!appKey) { - roundResult = bundlePreparationResult('failed', 'invalid_config'); + const read = readNativeConfig(context); + roundConfigJson = read.json; + if (read.rejection !== undefined) { + roundResult = read.rejection; return; } + const configJson = read.json; + const config = read.config; + const appKey = read.appKey; // 从这里起本轮开始做真实工作:留下面包屑,死于轮中时下次启动零延迟续传。 logger.info( TAG, diff --git a/harmony/pushy/src/main/ets/UpdateContext.ts b/harmony/pushy/src/main/ets/UpdateContext.ts index f30a3861..1d0ef6a8 100644 --- a/harmony/pushy/src/main/ets/UpdateContext.ts +++ b/harmony/pushy/src/main/ets/UpdateContext.ts @@ -27,6 +27,7 @@ import NativePatchCore, { StateCoreResult, } from './NativePatchCore'; import { assertSafePathComponent } from './PathUtils'; +import { withRawFd } from './SaveFile'; import { ERROR_SWITCH_VERSION_FAILED, createPushyError, @@ -48,6 +49,89 @@ interface OptionalPreferencesApi { getAllSync?: () => Record; } +// C1 跨平台配置等价(JS/Android/iOS 同一规则):缺省键补默认值、端点/ +// 发现地址去尾斜杠并保序去重、忽略键序。等价的配置不算"变更"——宿主重复 +// configure、或 JS 同步一份只差格式的同一配置,都不应取消在飞轮次或丢弃 +// 响应缓存。放在本文件(而非独立模块):本文件被测试原样求值,只能依赖 +// 自身源码。 +const NATIVE_CONFIG_DEFAULTS: Record = { + disabled: false, + queryUrls: [], + rnu: '', + rn: '', + afterDownload: 'none', +}; +const NATIVE_CONFIG_URL_LISTS: string[] = ['endpoints', 'queryUrls']; + +function canonicalJson(value: unknown): string { + if (Array.isArray(value)) { + return `[${value.map(item => canonicalJson(item)).join(',')}]`; + } + if (value !== null && typeof value === 'object') { + const record = value as Record; + const keys = Object.keys(record) + .filter(key => record[key] !== undefined) + .sort(); + return `{${keys + .map(key => `${JSON.stringify(key)}:${canonicalJson(record[key])}`) + .join(',')}}`; + } + return JSON.stringify(value) ?? 'null'; +} + +/** 配置 JSON 的规范形式;不是可解析的 JSON 对象时返回 undefined。 */ +export function canonicalNativeConfig(json: string): string | undefined { + let parsed: unknown; + try { + parsed = JSON.parse(json); + } catch (e) { + return undefined; + } + if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) { + return undefined; + } + const config: Record = {}; + const source = parsed as Record; + for (const key of Object.keys(source)) { + config[key] = source[key]; + } + for (const key of Object.keys(NATIVE_CONFIG_DEFAULTS)) { + if (config[key] === undefined) { + config[key] = NATIVE_CONFIG_DEFAULTS[key]; + } + } + for (const key of NATIVE_CONFIG_URL_LISTS) { + const list = config[key]; + if (!Array.isArray(list)) { + continue; + } + const seen = new Set(); + const normalized: unknown[] = []; + for (const item of list as unknown[]) { + const value = typeof item === 'string' ? item.replace(/\/+$/, '') : item; + const identity = canonicalJson(value); + if (!seen.has(identity)) { + seen.add(identity); + normalized.push(value); + } + } + config[key] = normalized; + } + return canonicalJson(config); +} + +/** 两份配置 JSON 是否等价(C1)。任一不是可解析的 JSON 对象即不等价。 */ +export function nativeConfigsEquivalent( + a: string | undefined, + b: string | undefined, +): boolean { + if (a === undefined || b === undefined) { + return false; + } + const left = canonicalNativeConfig(a); + return left !== undefined && left === canonicalNativeConfig(b); +} + interface PersistOptions { removeStaleHash?: boolean; cleanUp?: boolean; @@ -460,19 +544,43 @@ export class UpdateContext { this.flushInBackground('sync binary version'); } - /** 写入并落盘;flushSync 不可用时以 flush() 的结果拒绝。 */ - /** Shared JS/native configuration writer; all mutations precede the first await. */ private static nativeConfigGeneration: number = 0; + // 宿主本进程调用过 PushyFileJSBundleProvider.configure():配置归宿主所有, + // JS 的 syncNativeConfig 必须拒绝(C2 NATIVE_CONFIG_CONFLICT),而不是 + // 悄悄覆盖或忽略——接入方式不一致要大声失败。 + private static hostConfigured: boolean = false; public getNativeConfigGeneration(): number { return UpdateContext.nativeConfigGeneration; } + public isHostConfigured(): boolean { + return UpdateContext.hostConfigured; + } + + /** 宿主原生 configure() 的写入口:先标记归属,再走共用的写入。 */ + public setHostNativeConfig(config: string): Promise { + UpdateContext.hostConfigured = true; + return this.setNativeConfig(config); + } + + /** 与持久化的配置是否等价(C1)。 */ + public isStoredNativeConfig(config: string | undefined): boolean { + return nativeConfigsEquivalent(config, this.getKv(KEY_CONFIG)); + } + + /** + * Shared JS/native configuration writer; all mutations precede the first + * await. An equivalent configuration (C1) is not a change: the stored string, + * response cache and generations stay as they are. + */ public setNativeConfig(config: string): Promise { if (!this.getKv('uuid')) { this.preferences.putSync('uuid', util.generateRandomUUID()); } - if (this.getKv(KEY_CONFIG) !== config) { + const current = this.getKv(KEY_CONFIG); + // Byte-identical is trivially no change, even for a value C1 cannot parse. + if (!current || (current !== config && !nativeConfigsEquivalent(current, config))) { // Also guards A -> B -> A replacements and late download commits. UpdateContext.resetGeneration += 1; UpdateContext.nativeConfigGeneration += 1; @@ -484,6 +592,7 @@ export class UpdateContext { return this.flushPreferences('persist configuration'); } + /** 写入并落盘;flushSync 不可用时以 flush() 的结果拒绝。 */ public setKv(key: string, value: string): Promise { this.preferences.putSync(key, value); return this.flushPreferences(`set key ${key}`); @@ -952,29 +1061,56 @@ export class UpdateContext { } } + // 进行中的摘要计算:JS getBundleHash 与原生轮次可能同时要它,共用一次。 + private bundleHashTask: Promise | null = null; + /** * bundleHash = 二进制内嵌 JS bundle 的 sha256 —— 标识二进制本身,与当前运行 * 的热更版本无关。哈希对象与 pdiff 的源读取完全一致:rawfile 的 * bundle.harmony.js(DownloadTask.doPatchFromApp 同一读法)。懒计算 + 缓存; * 永不失败——空串表示"未知",服务端回退 buildTime 启发式。 */ - public async getBundleHash(): Promise { + public getBundleHash(): Promise { if (UpdateContext.DEBUG) { // debug 下 bundle 由 metro 提供,与 dev 删 buildTime 的行为对齐。 - return ''; + return Promise.resolve(''); } const cachePrefix = `${this.getPackageVersion()}|${this.getBundleModifiedTime()}|`; const cached = this.readString(UpdateContext.KEY_BUNDLE_HASH_CACHE); if (cached.startsWith(cachePrefix)) { - return cached.slice(cachePrefix.length); + return Promise.resolve(cached.slice(cachePrefix.length)); + } + if (!this.bundleHashTask) { + const task = this.computeBundleHash(cachePrefix); + this.bundleHashTask = task; + task.then( + () => { + this.bundleHashTask = null; + }, + () => { + this.bundleHashTask = null; + }, + ); } + return this.bundleHashTask; + } + + private async computeBundleHash(cachePrefix: string): Promise { let hash = ''; try { - // 异步读:TurboModule 跑在 UI 线程,同步读几 MB 会卡主线程一次。C++ - // 哈希本身毫秒级,同步无妨。 - const content = - await this.context.resourceManager.getRawFileContent('bundle.harmony.js'); - hash = NativePatchCore.sha256Hex(content); + // 内嵌 bundle 有几 MB:不读进内存,也不在 UI 线程(TurboModule 所在) + // 哈希——native 工作线程按 rawfile descriptor 的区间 pread 流式计算。 + // fd 在 promise 落定后才关闭(withRawFd)。 + hash = await withRawFd( + this.context.resourceManager, + 'bundle.harmony.js', + descriptor => + NativePatchCore.sha256HexRawFdAsync( + descriptor.fd, + descriptor.offset, + descriptor.length, + ), + ); } catch (error) { // rawfile 缺 bundle(未打包 release bundle)是合法的"未知",不是错误。 logger.info(TAG, `Cannot hash embedded bundle: ${getErrorMessage(error)}`); diff --git a/harmony/pushy/src/test/ArchiveLimits.test.ets b/harmony/pushy/src/test/ArchiveLimits.test.ets new file mode 100644 index 00000000..02c9b2e7 --- /dev/null +++ b/harmony/pushy/src/test/ArchiveLimits.test.ets @@ -0,0 +1,104 @@ +import { describe, it, expect } from '@ohos/hypium'; +import { + ORIGINAL_SIZE_UNAVAILABLE, + UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES, + checkOriginalSize, + planUnknownLengthFreeSpaceProbe, +} from '../main/ets/ArchiveLimits'; +import { ERROR_DELTA_FAILED, PushyError } from '../main/ets/ErrorCodes'; + +const PROBE = UNKNOWN_LENGTH_FREE_SPACE_PROBE_BYTES; + +function refusal(run: () => number): PushyError | null { + try { + run(); + } catch (e) { + return e as PushyError; + } + return null; +} + +export default function archiveLimitsTest() { + // 未知长度下载的磁盘探测:任何写入都不得越过已探测预留的位置。 + describe('planUnknownLengthFreeSpaceProbe', () => { + it('never probes when the total length is known', 0, () => { + const plan = planUnknownLengthFreeSpaceProbe(1000, 0, 4096, 0); + expect(plan.probe).assertFalse(); + expect(plan.reservedUntil).assertEqual(0); + }); + + it('probes the first chunk and reserves PROBE bytes ahead', 0, () => { + const plan = planUnknownLengthFreeSpaceProbe(0, 0, 4096, 0); + expect(plan.probe).assertTrue(); + expect(plan.reserve).assertEqual(PROBE); + expect(plan.reservedUntil).assertEqual(PROBE); + }); + + it('skips chunks that stay inside the reservation', 0, () => { + const plan = planUnknownLengthFreeSpaceProbe(0, 4096, 4096, PROBE); + expect(plan.probe).assertFalse(); + expect(plan.reservedUntil).assertEqual(PROBE); + // Ending exactly on the reserved boundary is still covered. + const edge = planUnknownLengthFreeSpaceProbe(0, PROBE - 10, 10, PROBE); + expect(edge.probe).assertFalse(); + }); + + it('re-probes a chunk that would cross the reservation', 0, () => { + const plan = planUnknownLengthFreeSpaceProbe(0, PROBE - 10, 11, PROBE); + expect(plan.probe).assertTrue(); + expect(plan.reservedUntil).assertEqual(PROBE - 10 + PROBE); + }); + + it('reserves a chunk larger than PROBE by its own size', 0, () => { + const big = PROBE * 2 + 1; + const plan = planUnknownLengthFreeSpaceProbe(0, 100, big, 0); + expect(plan.probe).assertTrue(); + expect(plan.reserve).assertEqual(big); + expect(plan.reservedUntil).assertEqual(100 + big); + }); + + it('walking a stream never writes past a probed reservation', 0, () => { + let reservedUntil = 0; + let written = 0; + let probes = 0; + const chunk = 300 * 1024; + for (let i = 0; i < 100; i++) { + const plan = planUnknownLengthFreeSpaceProbe(0, written, chunk, reservedUntil); + if (plan.probe) { + probes += 1; + } + reservedUntil = plan.reservedUntil; + written += chunk; + expect(written <= reservedUntil).assertTrue(); + } + expect(probes < 100).assertTrue(); + }); + }); + + // getOriginalSize 读不到:拒绝(不放行),且带专属错误细节可诊断。 + describe('checkOriginalSize', () => { + it('passes a finite non-negative size through', 0, () => { + expect(checkOriginalSize(0)).assertEqual(0); + expect(checkOriginalSize(12345)).assertEqual(12345); + }); + + it('refuses when getOriginalSize threw, with its own detail', 0, () => { + const error = refusal(() => checkOriginalSize(undefined, 'boom')); + expect(error !== null).assertTrue(); + expect(error!.code).assertEqual(ERROR_DELTA_FAILED); + expect(error!.message.includes(ORIGINAL_SIZE_UNAVAILABLE)).assertTrue(); + expect(error!.message.includes('getOriginalSize failed: boom')).assertTrue(); + }); + + it('refuses an unusable result, distinct from the thrown case', 0, () => { + const sizes: (number | undefined)[] = [undefined, NaN, -1, Infinity]; + for (const size of sizes) { + const error = refusal(() => checkOriginalSize(size)); + expect(error !== null).assertTrue(); + expect(error!.code).assertEqual(ERROR_DELTA_FAILED); + expect(error!.message.includes(ORIGINAL_SIZE_UNAVAILABLE)).assertTrue(); + expect(error!.message.includes('getOriginalSize returned')).assertTrue(); + } + }); + }); +} diff --git a/harmony/pushy/src/test/List.test.ets b/harmony/pushy/src/test/List.test.ets index 59c91747..d55166f9 100644 --- a/harmony/pushy/src/test/List.test.ets +++ b/harmony/pushy/src/test/List.test.ets @@ -5,6 +5,8 @@ import downloadTaskParamsTest from './DownloadTaskParams.test'; import contentRangeTest from './ContentRange.test'; import installRecordTest from './InstallRecord.test'; import errorCodesTest from './ErrorCodes.test'; +import archiveLimitsTest from './ArchiveLimits.test'; +import nativeConfigTest from './NativeConfig.test'; export default function testsuite() { manifestParsingTest(); @@ -14,4 +16,6 @@ export default function testsuite() { contentRangeTest(); installRecordTest(); errorCodesTest(); + archiveLimitsTest(); + nativeConfigTest(); } diff --git a/harmony/pushy/src/test/NativeConfig.test.ets b/harmony/pushy/src/test/NativeConfig.test.ets new file mode 100644 index 00000000..a727d43e --- /dev/null +++ b/harmony/pushy/src/test/NativeConfig.test.ets @@ -0,0 +1,59 @@ +import { describe, it, expect } from '@ohos/hypium'; +import { canonicalNativeConfig, nativeConfigsEquivalent } from '../main/ets/UpdateContext'; + +// C1:跨平台配置等价规则(与 JS/Android/iOS 一致)。 +export default function nativeConfigTest() { + describe('nativeConfigsEquivalent', () => { + it('ignores key order', 0, () => { + expect(nativeConfigsEquivalent( + '{"appKey":"k","endpoints":["https://a"]}', + '{"endpoints":["https://a"],"appKey":"k"}', + )).assertTrue(); + }); + + it('fills absent keys with their defaults', 0, () => { + expect(nativeConfigsEquivalent( + '{"appKey":"k"}', + '{"appKey":"k","disabled":false,"queryUrls":[],"rnu":"","rn":"","afterDownload":"none"}', + )).assertTrue(); + expect(nativeConfigsEquivalent('{"appKey":"k"}', '{"appKey":"k","disabled":true}')) + .assertFalse(); + expect(nativeConfigsEquivalent('{"appKey":"k"}', '{"appKey":"k","rn":"0.82"}')) + .assertFalse(); + }); + + it('strips trailing slashes and dedupes URL lists, keeping first order', 0, () => { + expect(nativeConfigsEquivalent( + '{"appKey":"k","endpoints":["https://a/","https://b","https://a"],"queryUrls":["https://q/"]}', + '{"appKey":"k","endpoints":["https://a","https://b"],"queryUrls":["https://q"]}', + )).assertTrue(); + expect(nativeConfigsEquivalent( + '{"appKey":"k","endpoints":["https://a","https://b"]}', + '{"appKey":"k","endpoints":["https://b","https://a"]}', + )).assertFalse(); + // Every trailing slash goes, not just one. + expect(nativeConfigsEquivalent( + '{"appKey":"k","endpoints":["https://b.example.com//"]}', + '{"appKey":"k","endpoints":["https://b.example.com"]}', + )).assertTrue(); + }); + + it('treats unparseable or non-object JSON as not equivalent', 0, () => { + expect(nativeConfigsEquivalent('{', '{"appKey":"k"}')).assertFalse(); + expect(nativeConfigsEquivalent('[]', '{}')).assertFalse(); + // Not even against itself: only JSON objects have a canonical form. + expect(nativeConfigsEquivalent('{', '{')).assertFalse(); + expect(nativeConfigsEquivalent('"x"', '"x"')).assertFalse(); + expect(nativeConfigsEquivalent(undefined, '{"appKey":"k"}')).assertFalse(); + expect(canonicalNativeConfig('null') === undefined).assertTrue(); + }); + + it('compares every other field exactly', 0, () => { + expect(nativeConfigsEquivalent('{"appKey":"k"}', '{"appKey":"k2"}')).assertFalse(); + expect(nativeConfigsEquivalent( + '{"appKey":"k","packageVersion":"1"}', + '{"appKey":"k"}', + )).assertFalse(); + }); + }); +} diff --git a/harmony/pushy/src/test/check-constant-parity.js b/harmony/pushy/src/test/check-constant-parity.js index b61cdd8e..0a997898 100644 --- a/harmony/pushy/src/test/check-constant-parity.js +++ b/harmony/pushy/src/test/check-constant-parity.js @@ -79,6 +79,9 @@ const pairs = [ header: 'cpp/patch_core/archive_limits.h', mirror: 'harmony/pushy/src/main/ets/ArchiveLimits.ts', mirrorName: (cppName) => screamingSnake(cppName), + // Must always be parsed from the header (guards the parser itself: a + // regex regression would otherwise silently drop the pair's coverage). + required: ['UnknownLengthFreeSpaceProbeBytes', 'MaxArchiveBytes'], }, { header: 'cpp/patch_core/install_record.h', @@ -109,6 +112,11 @@ for (const pair of pairs) { problems.push(`${pair.header}: no constexpr constants parsed`); continue; } + for (const name of pair.required || []) { + if (!cpp.has(name)) { + problems.push(`${pair.header}: k${name} not parsed (required)`); + } + } for (const [cppName, cppValue] of cpp) { const mirrorName = pair.mirrorName(cppName); checked += 1; From 11d8775029dab114ed9a22107d78fb13b20472a7 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:19:17 +0800 Subject: [PATCH 04/21] ci(e2e): build the iOS app once, share Android setup, tighten triggers e2e-ios: a new e2e-ios-build job does pod install + xcodebuild (or the cached-app bundle refresh) and prepares the local update artifacts once, then uploads the .app and .e2e-artifacts/ios as a tarball (tar keeps the executable bits upload-artifact drops). The core and native legs keep their job id and "e2e-ios (core|native)" check names, download the build and only run Detox. The e2e-cache action's trusted writer moves with the work to e2e-ios-build; legs restore read-only. e2e-android: the setup the three jobs duplicated (CLI checkout, node, bun, caches, Java, KVM, deps, local link) is now the e2e-android-setup composite action. The rn077 job keys its Gradle cache on the project generator and the library build.gradle explicitly; it keeps a plain `bun install` because the generated project has no lockfile to freeze. Pushes trigger on master only (main does not exist). Both: drop scripts/** and root bun.lock from the path filters (no e2e build or test step reads them; package.json stays for codegenConfig) and stop copying scripts/ into the linked package, which npm never ships either. Detox: the per-command --maxWorkers 1 moves into .detoxrc.js as an Android-only configuration override (one AVD cannot boot twice). iOS keeps its two workers on cloned simulators. Co-Authored-By: Claude Opus 5.5 --- .github/actions/e2e-android-setup/action.yml | 117 +++++++++ .github/actions/e2e-cache/action.yml | 8 +- .github/workflows/e2e_android.yml | 242 +++---------------- .github/workflows/e2e_ios.yml | 205 ++++++++++------ Example/e2etest/.detoxrc.js | 12 + 5 files changed, 302 insertions(+), 282 deletions(-) create mode 100644 .github/actions/e2e-android-setup/action.yml diff --git a/.github/actions/e2e-android-setup/action.yml b/.github/actions/e2e-android-setup/action.yml new file mode 100644 index 00000000..5163ad68 --- /dev/null +++ b/.github/actions/e2e-android-setup/action.yml @@ -0,0 +1,117 @@ +name: E2E Android setup +description: >- + Shared setup for the Android Detox jobs: toolchains, caches, KVM, the e2e + project's dependencies and the local react-native-update link. Runs after + the react-native-update checkout (this action lives in it). +inputs: + project-dir: + description: E2E app directory, relative to the workspace. + default: Example/e2etest + checkout-cli: + description: Check out and build react-native-update-cli (needed to prepare update artifacts). + default: 'true' + bun-cache-scope: + description: Extra bun cache key segment for jobs whose dependency set differs. + default: '' + bun-cache-key-suffix: + description: Extra hash appended to the bun cache key. + default: '' + gradle-cache-dependency-path: + description: Files whose hash keys the Gradle cache; they must exist before the e2e project is created. + default: | + Example/e2etest/bun.lock + Example/e2etest/android/*.gradle + Example/e2etest/android/**/*.gradle + Example/e2etest/android/gradle/wrapper/gradle-wrapper.properties + create-project: + description: Command that generates the e2e project before its dependencies are installed. + default: '' + frozen-lockfile: + description: Install with --frozen-lockfile (false only for generated projects that have no lockfile). + default: 'true' +runs: + using: composite + steps: + - name: Checkout react-native-update-cli + if: inputs.checkout-cli == 'true' + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + repository: reactnativecn/react-native-update-cli + path: react-native-update-cli + + - name: Setup Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 24 + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.4.0 + + - name: Cache Bun dependencies + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-e2e${{ inputs.bun-cache-scope }}-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }}${{ inputs.bun-cache-key-suffix }} + restore-keys: | + ${{ runner.os }}-bun-e2e${{ inputs.bun-cache-scope }}- + ${{ runner.os }}-bun-e2e- + + - name: Setup Java + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: '17' + cache: gradle + cache-dependency-path: ${{ inputs.gradle-cache-dependency-path }} + + - name: Enable KVM + shell: bash + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Create e2e project + if: inputs.create-project != '' + shell: bash + env: + CREATE_PROJECT: ${{ inputs.create-project }} + run: eval "$CREATE_PROJECT" + + - name: Install e2e project dependencies + shell: bash + working-directory: ${{ inputs.project-dir }} + env: + FROZEN: ${{ inputs.frozen-lockfile }} + run: | + if [ "$FROZEN" = 'true' ]; then + bun install --frozen-lockfile + else + bun install + fi + + - name: Link local react-native-update + # Mirrors what an npm install ships (package.json `files`), plus cpp/ + # for the iOS/Android native builds; scripts/ is dev-only and unused + # by any build step. + shell: bash + env: + E2E_DIR: ${{ inputs.project-dir }} + run: | + set -euo pipefail + export LOCAL_RNU_VERSION="$(node -p "require('./${E2E_DIR}/node_modules/react-native-update/package.json').version")" + export LOCAL_RNU_DIR="$PWD/$E2E_DIR/.e2e-artifacts/local-react-native-update" + rm -rf "$LOCAL_RNU_DIR" "$E2E_DIR/node_modules/react-native-update" + mkdir -p "$LOCAL_RNU_DIR" + cp package.json react-native-update.podspec react-native.config.js expo-module.config.json "$LOCAL_RNU_DIR"/ + rsync -a --exclude='.git' src ios android cpp "$LOCAL_RNU_DIR"/ + node -e "const fs = require('fs'); const file = process.env.LOCAL_RNU_DIR + '/package.json'; const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); pkg.version = process.env.LOCAL_RNU_VERSION; fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');" + ln -s ../.e2e-artifacts/local-react-native-update "$E2E_DIR/node_modules/react-native-update" + + - name: Install react-native-update-cli dependencies + if: inputs.checkout-cli == 'true' + shell: bash + working-directory: react-native-update-cli + run: bun install --frozen-lockfile && bun run build diff --git a/.github/actions/e2e-cache/action.yml b/.github/actions/e2e-cache/action.yml index 1352259f..ad8ebaa0 100644 --- a/.github/actions/e2e-cache/action.yml +++ b/.github/actions/e2e-cache/action.yml @@ -1,5 +1,5 @@ name: Bounded content-addressed E2E cache -description: Restore shared content caches; only mainline core saves a daily snapshot. +description: Restore shared content caches; only the mainline iOS build job saves a daily snapshot. inputs: path: description: Content-addressed cache directory, never a built app. @@ -8,7 +8,7 @@ inputs: description: Cache schema/toolchain identity. required: true write: - description: The core matrix leg may write on a trusted master push. + description: The e2e-ios-build job may write on a trusted master push. default: 'false' runs: using: composite @@ -17,7 +17,7 @@ runs: shell: bash run: echo "day=$(date -u +%Y%m%d)" >> "$GITHUB_OUTPUT" - name: Restore read-only snapshot - if: "!(inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios')" + if: "!(inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios-build')" uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ inputs.path }} @@ -25,7 +25,7 @@ runs: restore-keys: | e2e-content-v2-${{ runner.os }}-${{ runner.arch }}-${{ inputs.scope }}- - name: Restore and save trusted daily snapshot - if: inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios' + if: inputs.write == 'true' && github.event_name == 'push' && github.ref == 'refs/heads/master' && github.job == 'e2e-ios-build' uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: ${{ inputs.path }} diff --git a/.github/workflows/e2e_android.yml b/.github/workflows/e2e_android.yml index 9e9800f4..0a220f4b 100644 --- a/.github/workflows/e2e_android.yml +++ b/.github/workflows/e2e_android.yml @@ -4,36 +4,23 @@ on: pull_request: branches: - '**' - paths: + paths: &e2e-android-paths - '.github/workflows/e2e_android.yml' + - '.github/actions/e2e-android-setup/**' - '.gitmodules' + # codegenConfig is copied into the linked package and drives codegen - 'package.json' - - 'bun.lock' - 'react-native-update.podspec' - 'react-native.config.js' - 'expo-module.config.json' - 'src/**' - - 'scripts/**' - 'android/**' - 'cpp/**' - 'Example/e2etest/**' push: branches: - master - - main - paths: - - '.github/workflows/e2e_android.yml' - - '.gitmodules' - - 'package.json' - - 'bun.lock' - - 'react-native-update.podspec' - - 'react-native.config.js' - - 'expo-module.config.json' - - 'src/**' - - 'scripts/**' - - 'android/**' - - 'cpp/**' - - 'Example/e2etest/**' + paths: *e2e-android-paths concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -49,65 +36,8 @@ jobs: with: submodules: recursive - - name: Checkout react-native-update-cli - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: reactnativecn/react-native-update-cli - path: react-native-update-cli - - - name: Setup Node.js - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: 24 - - - name: Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version: 1.4.0 - - - name: Cache Bun dependencies - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-e2e-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-e2e- - - - name: Setup Java - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 - with: - distribution: temurin - java-version: '17' - cache: gradle - cache-dependency-path: | - Example/e2etest/bun.lock - Example/e2etest/android/*.gradle - Example/e2etest/android/**/*.gradle - Example/e2etest/android/gradle/wrapper/gradle-wrapper.properties - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Install e2etest dependencies - run: cd Example/e2etest && bun install --frozen-lockfile - - - name: Link local react-native-update - run: | - set -euo pipefail - export LOCAL_RNU_VERSION="$(node -p "require('./Example/e2etest/node_modules/react-native-update/package.json').version")" - LOCAL_RNU_DIR="$PWD/Example/e2etest/.e2e-artifacts/local-react-native-update" - rm -rf "$LOCAL_RNU_DIR" Example/e2etest/node_modules/react-native-update - mkdir -p "$LOCAL_RNU_DIR" - cp package.json react-native-update.podspec react-native.config.js expo-module.config.json "$LOCAL_RNU_DIR"/ - rsync -a --exclude='.git' src ios android cpp scripts "$LOCAL_RNU_DIR"/ - node -e "const fs = require('fs'); const file = 'Example/e2etest/.e2e-artifacts/local-react-native-update/package.json'; const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); pkg.version = process.env.LOCAL_RNU_VERSION; fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');" - ln -s ../.e2e-artifacts/local-react-native-update Example/e2etest/node_modules/react-native-update - - - name: Install react-native-update-cli dependencies - run: cd react-native-update-cli && bun install --frozen-lockfile && bun run build + - name: Set up e2e environment + uses: ./.github/actions/e2e-android-setup - name: Detox build (android.emu.release) env: @@ -135,19 +65,19 @@ jobs: avd-name: api34 # Pinned while stabilizing e2e. The 2026-07-30 breakage was NOT the # emulator version (red herring): bundle-hash.test.ts landed 07-29 as - # a second suite, and .detoxrc.js forces maxWorkers 2 on CI — the - # second Detox worker tries to boot the same AVD twice, which Android - # cannot do, and retries until the job times out (fixed with - # --maxWorkers 1 below). Unpin once a run proves 37.x stable. + # a second suite, and a second Detox worker tried to boot the same + # AVD twice, which Android cannot do, retrying until the job timed + # out. .detoxrc.js now pins the Android configurations to one + # worker. Unpin once a run proves 37.x stable. emulator-build: 15507667 emulator-boot-timeout: 900 # 每条命令自带 cd:android-emulator-runner 逐行起独立 shell, # 单独一行的 cd 不会保持到下一条(实测 detox 会在仓库根找不到 jest) script: | - cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 --maxWorkers 1 + cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 # 原生冷启动检测有自己的 runner 配置(见 e2e/native/jest.config.js); # Android 这个 job 有预算,不必像 iOS 那样拆成独立 job - cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 --maxWorkers 1 --config e2e/native/jest.config.js + cd Example/e2etest && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 --config e2e/native/jest.config.js - name: Upload Detox artifacts # cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场 @@ -172,56 +102,10 @@ jobs: with: submodules: recursive - - name: Setup Node.js - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: 24 - - - name: Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version: 1.4.0 - - - name: Cache Bun dependencies - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + - name: Set up e2e environment + uses: ./.github/actions/e2e-android-setup with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-e2e-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-e2e- - - - name: Setup Java - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 - with: - distribution: temurin - java-version: '17' - cache: gradle - cache-dependency-path: | - Example/e2etest/bun.lock - Example/e2etest/android/*.gradle - Example/e2etest/android/**/*.gradle - Example/e2etest/android/gradle/wrapper/gradle-wrapper.properties - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Install e2etest dependencies - run: cd Example/e2etest && bun install --frozen-lockfile - - - name: Link local react-native-update - run: | - set -euo pipefail - export LOCAL_RNU_VERSION="$(node -p "require('./Example/e2etest/node_modules/react-native-update/package.json').version")" - LOCAL_RNU_DIR="$PWD/Example/e2etest/.e2e-artifacts/local-react-native-update" - rm -rf "$LOCAL_RNU_DIR" Example/e2etest/node_modules/react-native-update - mkdir -p "$LOCAL_RNU_DIR" - cp package.json react-native-update.podspec react-native.config.js expo-module.config.json "$LOCAL_RNU_DIR"/ - rsync -a --exclude='.git' src ios android cpp scripts "$LOCAL_RNU_DIR"/ - node -e "const fs = require('fs'); const file = 'Example/e2etest/.e2e-artifacts/local-react-native-update/package.json'; const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); pkg.version = process.env.LOCAL_RNU_VERSION; fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');" - ln -s ../.e2e-artifacts/local-react-native-update Example/e2etest/node_modules/react-native-update + checkout-cli: 'false' - name: Detox build (android.emu.debug) env: @@ -248,12 +132,7 @@ jobs: target: google_apis arch: x86_64 avd-name: api34 - # Pinned while stabilizing e2e. The 2026-07-30 breakage was NOT the - # emulator version (red herring): bundle-hash.test.ts landed 07-29 as - # a second suite, and .detoxrc.js forces maxWorkers 2 on CI — the - # second Detox worker tries to boot the same AVD twice, which Android - # cannot do, and retries until the job times out (fixed with - # --maxWorkers 1 below). Unpin once a run proves 37.x stable. + # Pinned while stabilizing e2e; see the e2e-android job. emulator-build: 15507667 emulator-boot-timeout: 900 # 注意:emulator-runner 把 script 的每一行单独用 sh -c 执行, @@ -285,67 +164,23 @@ jobs: with: submodules: recursive - - name: Checkout react-native-update-cli - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - repository: reactnativecn/react-native-update-cli - path: react-native-update-cli - - - name: Setup Node.js - uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 - with: - node-version: 24 - - - name: Setup Bun - uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 - with: - bun-version: 1.4.0 - - - name: Cache Bun dependencies - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/.bun/install/cache - key: ${{ runner.os }}-bun-e2e-rn077-oldarch-${{ hashFiles('Example/e2etest/bun.lock', 'Example/e2etest/scripts/create-rn077-oldarch-project.js', 'react-native-update-cli/bun.lock') }} - restore-keys: | - ${{ runner.os }}-bun-e2e-rn077-oldarch- - ${{ runner.os }}-bun-e2e- - - - name: Setup Java - uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 - with: - distribution: temurin - java-version: '17' - cache: gradle - - - name: Enable KVM - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - name: Create RN 0.77.3 old architecture e2e project - run: node Example/e2etest/scripts/create-rn077-oldarch-project.js - - - name: Install RN 0.77.3 old architecture dependencies - run: cd .e2e-rn077-oldarch/AwesomeProject && bun install - - - name: Link local react-native-update - run: | - set -euo pipefail - E2E_DIR=".e2e-rn077-oldarch/AwesomeProject" - export LOCAL_RNU_VERSION="$(node -p "require('./${E2E_DIR}/node_modules/react-native-update/package.json').version")" - LOCAL_RNU_DIR="$PWD/$E2E_DIR/.e2e-artifacts/local-react-native-update" - rm -rf "$LOCAL_RNU_DIR" "$E2E_DIR/node_modules/react-native-update" - mkdir -p "$LOCAL_RNU_DIR" - cp package.json react-native-update.podspec react-native.config.js expo-module.config.json "$LOCAL_RNU_DIR"/ - rsync -a --exclude='.git' src ios android cpp scripts "$LOCAL_RNU_DIR"/ - node -e "const fs = require('fs'); const file = process.env.LOCAL_RNU_DIR + '/package.json'; const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); pkg.version = process.env.LOCAL_RNU_VERSION; fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');" - ln -s ../.e2e-artifacts/local-react-native-update "$E2E_DIR/node_modules/react-native-update" - env: - LOCAL_RNU_DIR: ${{ github.workspace }}/.e2e-rn077-oldarch/AwesomeProject/.e2e-artifacts/local-react-native-update - - - name: Install react-native-update-cli dependencies - run: cd react-native-update-cli && bun install --frozen-lockfile && bun run build + - name: Set up e2e environment + uses: ./.github/actions/e2e-android-setup + with: + project-dir: .e2e-rn077-oldarch/AwesomeProject + bun-cache-scope: -rn077-oldarch + bun-cache-key-suffix: -${{ hashFiles('Example/e2etest/scripts/create-rn077-oldarch-project.js') }} + # The project is generated by the script below (RN/template/Detox + # versions live there) after the cache is restored, so key the + # Gradle cache on its inputs rather than on files that do not exist + # yet; the library module's own build.gradle participates too. + gradle-cache-dependency-path: | + Example/e2etest/scripts/create-rn077-oldarch-project.js + android/build.gradle + create-project: node Example/e2etest/scripts/create-rn077-oldarch-project.js + # `react-native init --skip-install` writes no lockfile, so there is + # nothing to freeze against. + frozen-lockfile: 'false' - name: Detox build (RN 0.77.3 old arch android.emu.release) env: @@ -371,18 +206,13 @@ jobs: target: google_apis arch: x86_64 avd-name: api34 - # Pinned while stabilizing e2e. The 2026-07-30 breakage was NOT the - # emulator version (red herring): bundle-hash.test.ts landed 07-29 as - # a second suite, and .detoxrc.js forces maxWorkers 2 on CI — the - # second Detox worker tries to boot the same AVD twice, which Android - # cannot do, and retries until the job times out (fixed with - # --maxWorkers 1 below). Unpin once a run proves 37.x stable. + # Pinned while stabilizing e2e; see the e2e-android job. emulator-build: 15507667 emulator-boot-timeout: 900 # 每条命令自带 cd,理由同上 script: | - cd .e2e-rn077-oldarch/AwesomeProject && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 --maxWorkers 1 - cd .e2e-rn077-oldarch/AwesomeProject && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 --maxWorkers 1 --config e2e/native/jest.config.js + cd .e2e-rn077-oldarch/AwesomeProject && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 + cd .e2e-rn077-oldarch/AwesomeProject && E2E_PLATFORM=android bunx detox test --configuration android.emu.release --headless --record-logs all --retries 1 --config e2e/native/jest.config.js - name: Upload Detox artifacts # cancelled() 覆盖 job 超时被杀的场景,否则超时时拿不到现场 diff --git a/.github/workflows/e2e_ios.yml b/.github/workflows/e2e_ios.yml index 468dabcb..433d937d 100644 --- a/.github/workflows/e2e_ios.yml +++ b/.github/workflows/e2e_ios.yml @@ -4,17 +4,16 @@ on: pull_request: branches: - '**' - paths: + paths: &e2e-ios-paths - '.github/workflows/e2e_ios.yml' - '.github/actions/e2e-cache/**' - '.gitmodules' + # codegenConfig is copied into the linked package and drives codegen - 'package.json' - - 'bun.lock' - 'react-native-update.podspec' - 'react-native.config.js' - 'expo-module.config.json' - 'src/**' - - 'scripts/**' - 'ios/**' - 'cpp/**' - 'android/jni/**' @@ -22,47 +21,20 @@ on: push: branches: - master - paths: - - '.github/workflows/e2e_ios.yml' - - '.github/actions/e2e-cache/**' - - '.gitmodules' - - 'package.json' - - 'bun.lock' - - 'react-native-update.podspec' - - 'react-native.config.js' - - 'expo-module.config.json' - - 'src/**' - - 'scripts/**' - - 'ios/**' - - 'cpp/**' - - 'android/jni/**' - - 'Example/e2etest/**' + paths: *e2e-ios-paths concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - e2e-ios: - # 不带 matrix 值全量拼名,否则 check 叫 "e2e-ios (native, --config e2e/…)" - name: e2e-ios (${{ matrix.suite }}) + # Builds the release .app once (pod install + xcodebuild, or the cached app + # with a refreshed JS bundle) and prepares the local update artifacts; both + # test legs below install that same build instead of compiling it twice. + e2e-ios-build: + name: e2e-ios (build) runs-on: macos-26 - # 绿色基线 ~16 分钟。60 分钟覆盖最坏情况:一个用例等满 300s testTimeout, - # 再叠一次 --retries 1 全量重试。40 分钟不够——实测重试跑到一半被腰斩, - # 把"慢但能恢复"的运行变成 cancelled,等于白开了 retries。 - timeout-minutes: 60 - strategy: - # 两条 leg 各自独立成败:一条超时不该掩盖另一条的结论 - fail-fast: false - matrix: - include: - # 既有 suite(local-merge / bundle-hash),沿用默认 runner 配置 - - suite: core - jest-config: '' - # 原生冷启动检测:每条断言都要真实重启 app,单独跑才不会把 core - # 顶过 40 分钟(拆分前实测 core 已占 36 分) - - suite: native - jest-config: '--config e2e/native/jest.config.js' + timeout-minutes: 45 steps: - name: Checkout react-native-update uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -98,45 +70,36 @@ jobs: with: xcode-version: '26.4' - - name: Boot iOS simulator in background - # 模拟器冷启动要 2-5 分钟,提前异步 boot,让它和依赖安装/xcodebuild - # 并行。型号必须用与 .detoxrc.ts 完全同一份探测代码——此前 shell 里 - # 另写一份选中了 detox 不用的设备,预热全部白费。 - # 失败无害:detox 测试阶段会自己 boot。 - run: | - udid=$(node Example/e2etest/scripts/detect-ios-simulator-type.js --udid) - if [ -n "$udid" ]; then - echo "pre-booting $(node Example/e2etest/scripts/detect-ios-simulator-type.js) ($udid)" - xcrun simctl boot "$udid" || true - else - echo "no matching simulator found, skipping pre-boot" - fi - - name: Cache Homebrew downloads - # Bottle content addressing still controls reuse; only snapshot writes - # are reduced to the successful master/core run's daily identity. + # Bottle content addressing still controls reuse; only this job's + # master runs write the daily snapshot. applesimutils is installed + # here too (unused by the build) so the snapshot the test legs + # restore holds every bottle they need. uses: ./.github/actions/e2e-cache with: path: ~/Library/Caches/Homebrew scope: brew-xcode-26.4 - write: ${{ matrix.suite == 'core' }} + write: 'true' - name: Install applesimutils and ccache run: brew trust wix/brew && HOMEBREW_NO_AUTO_UPDATE=1 brew install wix/brew/applesimutils ccache - name: Cache ccache # Old content-addressed entries affect hit rate, not build correctness. - # PRs and the native matrix leg only restore; no per-commit duplicates. + # PRs only restore; no per-commit duplicates. uses: ./.github/actions/e2e-cache with: path: ~/Library/Caches/ccache scope: ccache-xcode-26.4 - write: ${{ matrix.suite == 'core' }} + write: 'true' - name: Install e2etest dependencies run: cd Example/e2etest && bun install --frozen-lockfile - name: Link local react-native-update + # Mirrors what an npm install ships (package.json `files`), plus cpp/ + # for the native build; scripts/ is dev-only and unused by any build + # step. run: | set -euo pipefail export LOCAL_RNU_VERSION="$(node -p "require('./Example/e2etest/node_modules/react-native-update/package.json').version")" @@ -144,7 +107,7 @@ jobs: rm -rf "$LOCAL_RNU_DIR" Example/e2etest/node_modules/react-native-update mkdir -p "$LOCAL_RNU_DIR" cp package.json react-native-update.podspec react-native.config.js expo-module.config.json "$LOCAL_RNU_DIR"/ - rsync -a --exclude='.git' src ios android cpp scripts "$LOCAL_RNU_DIR"/ + rsync -a --exclude='.git' src ios android cpp "$LOCAL_RNU_DIR"/ node -e "const fs = require('fs'); const file = 'Example/e2etest/.e2e-artifacts/local-react-native-update/package.json'; const pkg = JSON.parse(fs.readFileSync(file, 'utf8')); pkg.version = process.env.LOCAL_RNU_VERSION; fs.writeFileSync(file, JSON.stringify(pkg, null, 2) + '\n');" ln -s ../.e2e-artifacts/local-react-native-update Example/e2etest/node_modules/react-native-update @@ -157,7 +120,7 @@ jobs: with: path: Example/e2etest/.metro-cache scope: metro-xcode-26.4 - write: ${{ matrix.suite == 'core' }} + write: 'true' - name: Prepare local update artifacts env: @@ -194,19 +157,6 @@ jobs: # RN 自带的 ccache 编译器包装脚本 run: cd Example/e2etest/ios && USE_CCACHE=1 pod install - - name: Cache Detox iOS framework - id: cache-detox-ios-framework - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - with: - path: ~/Library/Detox - key: ${{ runner.os }}-detox-ios-framework-xcode-26.4-${{ hashFiles('Example/e2etest/bun.lock') }} - restore-keys: | - ${{ runner.os }}-detox-ios-framework-xcode-26.4- - - - name: Build Detox iOS framework cache - if: steps.cache-detox-ios-framework.outputs.cache-hit != 'true' - run: cd Example/e2etest && bunx detox build-framework-cache - - name: Detox build (ios.sim.release) if: steps.cache-ios-app.outputs.cache-hit != 'true' env: @@ -220,9 +170,120 @@ jobs: if: steps.cache-ios-app.outputs.cache-hit == 'true' run: cd Example/e2etest && ./scripts/refresh-ios-app-bundle.sh + - name: Pack app and update artifacts + # tar keeps the executable bits and symlinks that upload-artifact + # would drop. hermesc stays behind: only the bundle refresh above + # needs it. + run: | + tar -cf e2e-ios-build.tar \ + Example/e2etest/ios/build/Build/Products/Release-iphonesimulator/AwesomeProject.app \ + Example/e2etest/.e2e-artifacts/ios + + - name: Upload app and update artifacts + uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + with: + name: e2e-ios-build + path: e2e-ios-build.tar + if-no-files-found: error + retention-days: 1 + + e2e-ios: + # 不带 matrix 值全量拼名,否则 check 叫 "e2e-ios (native, --config e2e/…)" + name: e2e-ios (${{ matrix.suite }}) + needs: e2e-ios-build + runs-on: macos-26 + # 绿色基线(不含构建)约 10 分钟。60 分钟覆盖最坏情况:一个用例等满 300s + # testTimeout,再叠一次 --retries 1 全量重试。40 分钟不够——实测重试跑到 + # 一半被腰斩,把"慢但能恢复"的运行变成 cancelled,等于白开了 retries。 + timeout-minutes: 60 + strategy: + # 两条 leg 各自独立成败:一条超时不该掩盖另一条的结论 + fail-fast: false + matrix: + include: + # 既有 suite(local-merge / bundle-hash),沿用默认 runner 配置 + - suite: core + jest-config: '' + # 原生冷启动检测:每条断言都要真实重启 app,单独跑才不会把 core + # 顶过 40 分钟(拆分前实测 core 已占 36 分) + - suite: native + jest-config: '--config e2e/native/jest.config.js' + steps: + - name: Checkout react-native-update + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Node.js + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 + with: + node-version: 22 + + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version: 1.4.0 + + - name: Cache Bun dependencies + uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-e2e-${{ hashFiles('Example/e2etest/bun.lock', 'react-native-update-cli/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-e2e- + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@ed7a3b1fda3918c0306d1b724322adc0b8cc0a90 # v1.7.0 + with: + xcode-version: '26.4' + + - name: Boot iOS simulator in background + # 模拟器冷启动要 2-5 分钟,提前异步 boot,让它和依赖安装/下载并行。 + # 型号必须用与 .detoxrc.js 完全同一份探测代码——此前 shell 里 + # 另写一份选中了 detox 不用的设备,预热全部白费。 + # 失败无害:detox 测试阶段会自己 boot。 + run: | + udid=$(node Example/e2etest/scripts/detect-ios-simulator-type.js --udid) + if [ -n "$udid" ]; then + echo "pre-booting $(node Example/e2etest/scripts/detect-ios-simulator-type.js) ($udid)" + xcrun simctl boot "$udid" || true + else + echo "no matching simulator found, skipping pre-boot" + fi + + - name: Cache Homebrew downloads + uses: ./.github/actions/e2e-cache + with: + path: ~/Library/Caches/Homebrew + scope: brew-xcode-26.4 + + - name: Install applesimutils + run: brew trust wix/brew && HOMEBREW_NO_AUTO_UPDATE=1 brew install wix/brew/applesimutils + + - name: Install e2etest dependencies + run: cd Example/e2etest && bun install --frozen-lockfile + + - name: Download app and update artifacts + uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2 + with: + name: e2e-ios-build + + - name: Unpack app and update artifacts + run: tar -xf e2e-ios-build.tar && rm e2e-ios-build.tar + + - name: Cache Detox iOS framework + id: cache-detox-ios-framework + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/Library/Detox + key: ${{ runner.os }}-detox-ios-framework-xcode-26.4-${{ hashFiles('Example/e2etest/bun.lock') }} + restore-keys: | + ${{ runner.os }}-detox-ios-framework-xcode-26.4- + + - name: Build Detox iOS framework cache + if: steps.cache-detox-ios-framework.outputs.cache-hit != 'true' + run: cd Example/e2etest && bunx detox build-framework-cache + - name: Detox test (ios.sim.release) env: - RNU_CLI_ROOT: ${{ github.workspace }}/react-native-update-cli RNU_E2E_SKIP_PREPARE: 'true' run: cd Example/e2etest && E2E_PLATFORM=ios bunx detox test --configuration ios.sim.release --retries 1 ${{ matrix.jest-config }} diff --git a/Example/e2etest/.detoxrc.js b/Example/e2etest/.detoxrc.js index c028b31c..ee23e3a1 100644 --- a/Example/e2etest/.detoxrc.js +++ b/Example/e2etest/.detoxrc.js @@ -49,6 +49,12 @@ const androidArchitectures = process.env.DETOX_ANDROID_ARCHS const iosBuildBase = 'xcodebuild -workspace ios/AwesomeProject.xcworkspace -UseNewBuildSystem=NO -scheme AwesomeProject -sdk iphonesimulator -destination "generic/platform=iOS Simulator" -derivedDataPath ios/build'; +// One AVD can only be booted once: a second Detox worker on Android retries +// booting the same emulator until the job times out (the 2026-07-30 e2e +// breakage). Pin Android to a single worker here instead of on every +// `detox test` command line. +const androidRunner = { args: { maxWorkers: 1 } }; + /** @type {import('detox').DetoxConfig} */ const config = { logger: { @@ -57,6 +63,8 @@ const config = { testRunner: { args: { config: 'e2e/jest.config.js', + // iOS on CI: two workers, each on its own cloned simulator. Android + // overrides this to 1 (androidRunner below). maxWorkers: process.env.CI ? 2 : undefined, _: ['e2e'], }, @@ -128,18 +136,22 @@ const config = { 'android.att.debug': { device: 'attached', app: 'android.debug', + testRunner: androidRunner, }, 'android.att.release': { device: 'attached', app: 'android.release', + testRunner: androidRunner, }, 'android.emu.debug': { device: 'emulator', app: 'android.debug', + testRunner: androidRunner, }, 'android.emu.release': { device: 'emulator', app: 'android.release', + testRunner: androidRunner, }, }, }; From 022ca835ebd0b0d64520a8bf575d68e4d67a658e Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:19:17 +0800 Subject: [PATCH 05/21] ci: path-gate native unit tests, dedupe parity check, notify harmony-build test.yml: a dorny/paths-filter job gates cpp-test (cpp/, android/jni/, scripts/test-*) and the macOS ios-purge-restore-test (ios/, cpp/, its script and fixtures). Skipped jobs report success, so required checks and native-gate (which reads the workflow conclusion) stay satisfied. The fuzz step runs apt-get update before installing clang. lint.yml no longer runs check-native-spec-parity.js a second time; bun lint already ends with it. harmony-build keeps its own run because it does not run bun lint. ci-failure-email also watches harmony-build. Dependabot now scans the local composite actions, which pin SHAs as well. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 5 +++- .github/workflows/ci-failure-email.yml | 3 +++ .github/workflows/lint.yml | 8 +++--- .github/workflows/test.yml | 37 ++++++++++++++++++++++++++ 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 7058cefb..4c6e1f66 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -2,8 +2,11 @@ version: 2 updates: # Actions are pinned to commit SHAs in the workflows; Dependabot keeps the # SHA + version comment pairs current instead of a human copying hashes. + # The local composite actions pin SHAs too and are not scanned from "/". - package-ecosystem: github-actions - directory: / + directories: + - / + - /.github/actions/* schedule: interval: weekly groups: diff --git a/.github/workflows/ci-failure-email.yml b/.github/workflows/ci-failure-email.yml index 09d50c2a..9a0f245d 100644 --- a/.github/workflows/ci-failure-email.yml +++ b/.github/workflows/ci-failure-email.yml @@ -2,9 +2,12 @@ name: CI Failure Issue on: workflow_run: + # native-gate is left out on purpose: it only posts a commit status and + # its own run stays green whatever that status is. workflows: - e2e-android - e2e-ios + - harmony-build - lint - Publish Package to npmjs - test diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 09c1474e..1b4ad709 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -28,13 +28,11 @@ jobs: - name: Install Dependency run: bun install --frozen-lockfile + # `bun lint` ends with check-native-spec-parity.js: every + # src/NativePushy.ts method must be exported by all four native + # bindings, with the right sync/async kind on Harmony. - name: Run lint run: bun lint - name: Verify publish file list run: node scripts/check-packlist.js - - # Every src/NativePushy.ts method must be exported by all four native - # bindings, with the right sync/async kind on Harmony. - - name: Verify JS <-> native method parity - run: node scripts/check-native-spec-parity.js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6e15a93a..05912f00 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,38 @@ concurrency: cancel-in-progress: true jobs: + # The native jobs below only run when their inputs change. A job skipped by + # its `if:` still reports success, so required checks and native-gate (which + # looks at the workflow conclusion) are satisfied on unrelated changes. + changes: + name: Detect changed native inputs + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + outputs: + cpp: ${{ steps.filter.outputs.cpp }} + ios: ${{ steps.filter.outputs.ios }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - id: filter + uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + with: + filters: | + cpp: + - '.github/workflows/test.yml' + - '.gitmodules' + - 'cpp/**' + - 'android/jni/**' + - 'scripts/test-*' + ios: + - '.github/workflows/test.yml' + - 'ios/**' + - 'cpp/**' + - 'scripts/test-ios-purge-restore.sh' + - 'scripts/tests/ios-purge-restore/**' + js-test: name: JS Unit Tests runs-on: ubuntu-latest @@ -34,6 +66,8 @@ jobs: cpp-test: name: C++ Unit Tests + needs: changes + if: needs.changes.outputs.cpp == 'true' runs-on: ubuntu-latest timeout-minutes: 10 @@ -76,12 +110,15 @@ jobs: # libFuzzer is unavailable on the runner. - name: Fuzz parsers (bounded) run: | + sudo apt-get update sudo apt-get install -y clang FUZZ=1 ./scripts/test-update-flow-core.sh FUZZ=1 ./scripts/test-patch-core.sh ios-purge-restore-test: name: Native purge-restore ordering + needs: changes + if: needs.changes.outputs.ios == 'true' runs-on: macos-latest timeout-minutes: 5 From 83a6b0630df519968d16849e63a4de9a6da7fa45 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:19:17 +0800 Subject: [PATCH 06/21] ci(publish): require native-required before publishing The release commit must carry a green native-required status (posted by native-gate.yml) before either publish job can run; pending or not yet posted is waited out for up to 45 minutes. The full-history checkout uses a blobless filter since only tags/commits/trees are read, and the HAR comment now points at the files whitelist instead of the .npmignore. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/publish.yml | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index be64c47f..04da5855 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -9,17 +9,46 @@ env: permissions: id-token: write # Required for OIDC contents: read + statuses: read # native-required gate check jobs: check_changes: runs-on: ubuntu-latest # npm --provenance requires a GitHub-hosted runner + # Room for the native-required wait below. + timeout-minutes: 60 outputs: harmony_changed: ${{ steps.check.outputs.changed }} prev_tag: ${{ steps.check.outputs.prev_tag }} steps: + # Both publish jobs need this job, so a release can only ship a commit + # whose lint/test/native E2E gate (native-gate.yml posts it as the + # "native-required" commit status) is green. Pending or not yet posted + # (a release cut right after the push) is waited out; failure, error + # or a status that never shows up fails the release. + - name: Require native-required to be green for the release commit + shell: bash + env: + GH_TOKEN: ${{ github.token }} + SHA: ${{ github.sha }} + run: | + for attempt in $(seq 1 45); do + state="$(gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/status" \ + --jq '[.statuses[] | select(.context == "native-required")][0].state // "missing"')" + echo "native-required for $SHA: $state" + case "$state" in + success) exit 0 ;; + pending|missing) sleep 60 ;; + *) echo "::error::native-required is $state for $SHA; fix CI before publishing"; exit 1 ;; + esac + done + echo "::error::native-required for $SHA did not turn green within 45 minutes" + exit 1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + # Only tags, commits and trees are read (git tag --merged, git diff + # --name-only); skip the blobs of the whole history. + filter: blob:none - name: Check Harmony changes id: check shell: bash @@ -174,7 +203,8 @@ jobs: - name: Verify Android native libraries run: node scripts/verify-android-so.js # Fail closed: the package must always ship harmony/pushy.har - # (.npmignore explicitly whitelists it). If the previous release can't + # (package.json `files` lists it and check-packlist.js --release + # requires it). If the previous release can't # be fetched, fix the cause or re-run — never publish a HAR-less package. - name: Fetch previous Harmony HAR shell: bash From a15a2f11b7a04f443f506ad7537e415b3b8e1c2c Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:23:58 +0800 Subject: [PATCH 07/21] perf(android): cheaper hashing, IO and JNI; canonical config equality - Bundle hash computed once per process on its own thread (shared by JS getBundleHash and the native round) instead of queueing on the download executor behind cleanup/downloads. - Downloads: 64KB reads with emitCompleteSegments; archive SHA-256 computed while streaming (resume hashes the on-disk prefix first); full packages hash index.bundlejs during unzip. Activation re-hash kept (tamper check). 64KB copy buffers in SafeZipFile/UpdateFileUtils. - JNI: registration failures are logged; result classes, constructors and field IDs cached once in JNI_OnLoad; a missing member logs and makes the native throw instead of defaulting fields. - Cleanup keeps current/last/running (String[] keep) and only renames doomed entries into .trash under versionFilesLock; deletion runs after release. - Native check response cache moved to its own SharedPreferences file (migrated); round-incomplete breadcrumb cleared in the commit itself. - getConstants: launch markers applied asynchronously; bundleSha256 cached at launch resolution. - One parseRunnableConfig for all config gates (appKey trimmed). - Canonical config equivalence (shared contract) for the JS-round dedupe and config-change invalidation; syncNativeConfig rejects with NATIVE_CONFIG_CONFLICT once the host called PushyRuntime.configure(). - Shared OkHttpClient base; stale proguard comment fixed. Co-Authored-By: Claude Opus 5.5 --- android/jni/Android.mk | 1 + android/lib/arm64-v8a/librnpushy.so | Bin 340520 -> 342896 bytes android/lib/armeabi-v7a/librnpushy.so | Bin 210796 -> 212920 bytes android/lib/x86/librnpushy.so | Bin 316500 -> 318960 bytes android/lib/x86_64/librnpushy.so | Bin 338168 -> 340816 bytes android/proguard.pro | 4 +- .../modules/update/CleanupKeepNames.java | 25 +- .../modules/update/ConfigEquivalence.java | 128 ++++++ .../modules/update/DownloadTask.java | 94 ++++- .../modules/update/ErrorCodes.java | 1 + .../modules/update/HttpClients.java | 21 + .../modules/update/PushyRuntime.java | 15 +- .../modules/update/SafeZipFile.java | 27 +- .../modules/update/SyncCoordinator.java | 135 +++--- .../modules/update/UpdateContext.java | 227 ++++++++-- .../modules/update/UpdateFileUtils.java | 44 +- .../modules/update/UpdateModuleImpl.java | 16 +- .../modules/update/CleanupKeepNamesTest.java | 13 +- .../modules/update/ConfigEquivalenceTest.java | 57 +++ cpp/patch_core/digest.h | 9 +- cpp/patch_core/error_codes.h | 4 + cpp/patch_core/jni_log.h | 31 ++ cpp/patch_core/jni_natives.h | 8 +- cpp/patch_core/jni_registration.cpp | 14 +- cpp/patch_core/patch_core.cpp | 61 ++- cpp/patch_core/patch_core.h | 14 + cpp/patch_core/patch_core_android.cpp | 21 +- cpp/patch_core/tests/patch_core_test.cpp | 42 ++ cpp/patch_core/update_core_android.cpp | 393 ++++++++++-------- 29 files changed, 1050 insertions(+), 355 deletions(-) create mode 100644 android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java create mode 100644 android/src/main/java/cn/reactnative/modules/update/HttpClients.java create mode 100644 android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java create mode 100644 cpp/patch_core/jni_log.h diff --git a/android/jni/Android.mk b/android/jni/Android.mk index a3576c4c..5de674f7 100644 --- a/android/jni/Android.mk +++ b/android/jni/Android.mk @@ -5,6 +5,7 @@ include $(CLEAR_VARS) LOCAL_MODULE := rnpushy LOCAL_CPPFLAGS += -std=c++17 LOCAL_LDFLAGS += -Wl,--exclude-libs,ALL +LOCAL_LDLIBS := -llog LOCAL_C_INCLUDES := \ $(LOCAL_PATH) \ $(LOCAL_PATH)/HDiffPatch \ diff --git a/android/lib/arm64-v8a/librnpushy.so b/android/lib/arm64-v8a/librnpushy.so index 4a1a57896f8f10ada0be37808a8a3b7ff08aeb28..59a3ea9fd1f690a5340cd6d10739322b5cbbbc9e 100755 GIT binary patch delta 110067 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 z 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%0D^`;-vI;hcal-$0QgPns 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 diff --git a/android/lib/armeabi-v7a/librnpushy.so b/android/lib/armeabi-v7a/librnpushy.so index ae2d3516346a0404ea7e02e279a4d0bc64ffd8b5..e7e4a628c2b95d5d4a18f7e02b296ee9f1ff1aa6 100755 GIT binary patch delta 58383 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

8mXWpETAMitf;ua}d&+TsW!4(rhIOcPQS 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̎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 diff --git a/android/lib/x86/librnpushy.so b/android/lib/x86/librnpushy.so index 3373394290bb6e905638478fde6a7dfcfc466422..e6b28b842986bbbb89cd0832b9afdacc0ad8a331 100755 GIT binary patch delta 96998 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

w3MCa->XE)ypd^6&VV##xktO<>YiDC}^~hIN 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 Wfffau8Wed_QTO*(aBqpJ-2VX)K5StC delta 107368 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 z z`DuDX4vo&JO-e)T&Fq>aL-0}1zb45+_6?2DG2G04l#+y4o709QXs*TdED1TaqqfO1 z!Tz@y|J5JPB%?nZqmRk5r@gNcWNg#UpNu%tXy;^kQ7uZPv=sE(j&w6ceg%tg`V=V4 zW^{Q9Ob~zSHWhQxP}(|GZtz;vtFS2InB-Ak1&`;OMlsZsfD>S97aBGVZI5RnrXhwn zvQNdx##?c6WL|0u5;F2QQ(^FN8bAE<&ZD?iNXJ=kE|~*}`NK2<>buo3VM5 ziL@|HXUZrxS{Y&cXEAymcKR2~H8}Ec$wGcN(}FA*>3tHln)g_khVGE`C~yap6f%mO>%_NgbETC2Vjn zlT~f!ff29ed{_oej45=voU268Br*Un;Ja2YK&%HH;dp4A)?P0vc_N^maVc53!tJa+Hp( zfo{X%m1JG}*L|&>$l993+cWW0f31wSeG(4=*h*^=Efz!T&?N2Y!8$C;-u0#i>oJ^h z4!#~$9!>@8W!DDzy{x?;sd`jqvs$NQqvm`$DljR-#EFR=C4VNb@O=8Y z3|0h9`&_O>GdO>Nv=-90U&vbaCjUY_cO7cuPglN>P1w}_QZ{gG+5^&IOz?Eem*~4S z$Z>;gfULCKAUAn*h~uo_>waz1F&QQY=Uqa@vNei*n+J0`Bk(gI2WWf zfxi!$U^O_=1b+Cio@H+=62RctDEaM;`x~)ReM+6b!dyD~HI4jACaJGpQSMi=rh54m znKsEi4pm)_DML`yY&f4VY<4#qUm- zviT#*DL6)mYMhXcl&S}#S&~%#_c!2JNh|I~y3k0g= z;40`j5+734-f#&0mkYsrHf1@6FA)R34`S=W?rBST1Z%qJ6G2HTffJ7lC%UZ6MjlJ^ zRn3hrx~$0Y%1M8#xXk;k6E?4Hk&rIK54h{^FGOdh6e6J=c1XyU6UsE5#&6yrTn!t{ z{Wz*2vhgLC)yKWgrN2{>TVbDjryaH2EL*0H2BY7Z9E+M5kzpI15gG@a-^|6_`eUN< zCNZOxnjaW}A8j&Pe~g@WmOr*(Y7CYLzf%!8PgM8^=Q~5JD*Rl`S|rHHLq1BG*jc+h z4f|Fo??2Cs@@l!}OL_E<^P7t{%eg|G(8D}$y9^MjL#%o4_tHhNJ%_s zt~BfixzRR{PceeXX%~7e9ueFnYxlX*3Uvb0o11Drf>E+@3M8ks(Y#VvVhMgnXO42k z?M}(=(U}1~;E9i|M`r{?<5zUXC>S*1otSeKI5WdT_~%qQxeJ@&INaVPy*$E@Ow?%{ z{@}~|8Hsk8;lCGgk9dWrZM=3PH?^tzZW$W0C&I?2OUCpkc-H;Rr$>-*fh(jf*O^>q zaslL1v*Sm4xF2Fuk}JeyhS$O`OFkG*nabgw-2ByQ>3nqP=Zwm)UM z2VcCO#ZRE^k+ob8FcYpX;!oB@^v|15?f1x_^6tn`WM*r-{JZFU)-W4$)*jew^|tNS z;!U*Y&hI|5f;I+Pqc?*WeffC{j`6N#dvm1wvIS-Am8%0D?9ybuw$o&M>S{92ovku~ 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 ; } -keep class cn.reactnative.modules.update.CopyGroupResult { (); ; } -# JNI entry points are bound by their mangled Java names. +# Native methods are bound from JNI_OnLoad by class/method name (the encoded +# table in cpp/patch_core/jni_registration.cpp), not by exported symbols, so +# their classes and names must still survive shrinking unchanged. -keepclasseswithmembernames class cn.reactnative.modules.update.** { native ; } diff --git a/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java b/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java index a24388d2..50d37a35 100644 --- a/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java +++ b/android/src/main/java/cn/reactnative/modules/update/CleanupKeepNames.java @@ -1,25 +1,24 @@ package cn.reactnative.modules.update; -/** Selects the at-most-two distinct version names supported by cleanup JNI. */ +import java.util.LinkedHashSet; + +/** + * The distinct version names cleanup must never touch: the persisted current + * and last versions AND the version this process booted from (two switches + * without a restart otherwise evict the running bundle while its on-demand + * assets are still being served). + */ final class CleanupKeepNames { private CleanupKeepNames() { } static String[] select(String current, String previous, String running) { - String[] keep = new String[2]; + LinkedHashSet keep = new LinkedHashSet<>(); for (String candidate : new String[] {current, previous, running}) { - if (candidate == null || candidate.isEmpty() - || candidate.equals(keep[0]) || candidate.equals(keep[1])) { - continue; - } - if (keep[0] == null) { - keep[0] = candidate; - } else if (keep[1] == null) { - keep[1] = candidate; - } else { - return null; + if (candidate != null && !candidate.isEmpty()) { + keep.add(candidate); } } - return keep; + return keep.toArray(new String[0]); } } diff --git a/android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java b/android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java new file mode 100644 index 00000000..c21ea047 --- /dev/null +++ b/android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java @@ -0,0 +1,128 @@ +package cn.reactnative.modules.update; + +import androidx.annotation.Nullable; +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.json.JSONTokener; + +/** + * Cross-platform canonical equality of native-check config JSON strings + * (shared contract with the JS / iOS / Harmony sides). Two configs are + * equivalent iff their canonical forms deep-equal: + *

+ * Anything that is not a JSON object string is never equivalent to anything. + */ +final class ConfigEquivalence { + private static final Object NULL_VALUE = new Object(); + + private ConfigEquivalence() { + } + + static boolean equivalent(@Nullable String a, @Nullable String b) { + Map left = canonical(a); + if (left == null) { + return false; + } + Map right = canonical(b); + return right != null && left.equals(right); + } + + @Nullable + static Map canonical(@Nullable String json) { + if (json == null) { + return null; + } + Object parsed; + try { + parsed = new JSONTokener(json).nextValue(); + } catch (JSONException e) { + return null; + } + if (!(parsed instanceof JSONObject)) { + return null; + } + @SuppressWarnings("unchecked") + Map result = (Map) toCanonical(parsed); + putDefault(result, "disabled", Boolean.FALSE); + putDefault(result, "queryUrls", new ArrayList()); + putDefault(result, "rnu", ""); + putDefault(result, "rn", ""); + putDefault(result, "afterDownload", "none"); + normalizeUrls(result, "endpoints"); + normalizeUrls(result, "queryUrls"); + return result; + } + + private static void putDefault(Map map, String key, Object value) { + if (!map.containsKey(key)) { + map.put(key, value); + } + } + + private static void normalizeUrls(Map map, String key) { + Object value = map.get(key); + if (!(value instanceof List)) { + return; + } + LinkedHashSet unique = new LinkedHashSet<>(); + for (Object item : (List) value) { + unique.add(item instanceof String ? stripTrailingSlashes((String) item) : item); + } + map.put(key, new ArrayList(unique)); + } + + private static String stripTrailingSlashes(String url) { + int end = url.length(); + while (end > 0 && url.charAt(end - 1) == '/') { + end--; + } + return url.substring(0, end); + } + + // JSONObject/JSONArray have identity equals; convert to Map/List with + // value semantics. Numbers compare by value (1 == 1.0). + private static Object toCanonical(Object value) { + if (value instanceof JSONObject) { + JSONObject object = (JSONObject) value; + Map map = new HashMap<>(); + for (Iterator keys = object.keys(); keys.hasNext();) { + String key = keys.next(); + map.put(key, toCanonical(object.opt(key))); + } + return map; + } + if (value instanceof JSONArray) { + JSONArray array = (JSONArray) value; + List list = new ArrayList<>(array.length()); + for (int i = 0; i < array.length(); i++) { + list.add(toCanonical(array.opt(i))); + } + return list; + } + if (value instanceof Number) { + try { + return new BigDecimal(value.toString()).stripTrailingZeros(); + } catch (NumberFormatException e) { + return value.toString(); + } + } + if (value == null || value == JSONObject.NULL) { + return NULL_VALUE; + } + return value; + } +} diff --git a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java index 6d796cf3..f92ede86 100644 --- a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +++ b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java @@ -8,9 +8,12 @@ import com.facebook.react.bridge.WritableMap; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; +import java.security.DigestOutputStream; +import java.security.MessageDigest; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; @@ -33,7 +36,9 @@ class DownloadTask implements Runnable { private static final String BUNDLE_DELTA_ENTRY = Texts.reveal("3319f0d4b6c56a502c3b10fcdca0de7d4b3307e9"); - private static final int DOWNLOAD_CHUNK_SIZE = 4096; + // Read size per loop iteration (and the buffer for small in-memory + // reads); progress events are throttled separately. + private static final int DOWNLOAD_CHUNK_SIZE = 64 * 1024; // When the server does not report Content-Length we cannot key progress // events on percentage change, so throttle by bytes to avoid flooding the // bridge (e.g. a 20MB chunked download would otherwise emit ~5000 events). @@ -42,7 +47,7 @@ class DownloadTask implements Runnable { // slow-dripping connection could occupy the single-threaded download // executor indefinitely and starve queued tasks. The call timeout is a // generous upper bound sized for large full-package downloads. - private static final OkHttpClient HTTP_CLIENT = new OkHttpClient.Builder() + private static final OkHttpClient HTTP_CLIENT = HttpClients.base().newBuilder() .connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS) .readTimeout(30, java.util.concurrent.TimeUnit.SECONDS) .writeTimeout(30, java.util.concurrent.TimeUnit.SECONDS) @@ -62,8 +67,15 @@ private File stagingDirectory() { return InstallRecord.stagingDirectoryFor(params.unzipDirectory); } - // SHA-256 of the downloaded archive, computed right before extraction. + // SHA-256 of the downloaded archive, recorded in the install record. private String artifactSha256 = ""; + // The archive digest computed while its bytes streamed to disk (a resumed + // transfer first hashes the prefix already on disk); null when this task + // did not stream the whole file itself and it must be hashed from disk. + private String streamedArchiveSha256; + // Digest of index.bundlejs computed while a full package was extracted; + // null when the bundle was produced some other way (patch tasks). + private String stagedBundleSha256; private static final class PatchArchiveContents { final ArrayList entryNames = new ArrayList(); @@ -187,6 +199,7 @@ private void writeResumeMeta( private void downloadFile() throws IOException { this.hash = params.hash; + streamedArchiveSha256 = null; String url = params.url; File writePath = params.targetFile; File sidecar = resumeSidecarFile(writePath); @@ -364,10 +377,16 @@ private boolean transferArchive( // than the margin). A throw keeps the partial for a later attempt. long freeSpaceReservedUntil = 0; + // The archive digest is computed as the bytes reach the file, so + // the install does not re-read the whole archive afterwards. + MessageDigest digest = UpdateFileUtils.newSha256(); + if (append) { + UpdateFileUtils.updateDigest(digest, writePath); + } try ( BufferedSource source = body.source(); - BufferedSink sink = Okio.buffer( - append ? Okio.appendingSink(writePath) : Okio.sink(writePath)) + BufferedSink sink = Okio.buffer(Okio.sink(new DigestOutputStream( + new FileOutputStream(writePath, append), digest))) ) { while ((bytesRead = source.read(sink.buffer(), DOWNLOAD_CHUNK_SIZE)) != -1) { received += bytesRead; @@ -385,7 +404,9 @@ private boolean transferArchive( throw e; } } - sink.emit(); + // Write whole segments only: a 64KB read no longer costs + // a write() syscall per partially filled segment. + sink.emitCompleteSegments(); long overall = baseOffset + received; if (overall > ArchiveLimits.MAX_ARCHIVE_BYTES) { @@ -415,6 +436,7 @@ private boolean transferArchive( throw new IOException("Download incomplete: expected " + totalAll + " bytes, got " + writePath.length()); } + streamedArchiveSha256 = UpdateFileUtils.toHex(digest.digest()); // Final progress event, skipped when the loop already posted this // exact value (known length reaching 100% posts it in-loop). if (baseOffset + received != lastPostedBytes) { @@ -564,19 +586,38 @@ private void doFullPatch() throws IOException { File work = stagingDirectory(); UpdateFileUtils.removeDirectory(work); UpdateFileUtils.ensureDirectory(work); - artifactSha256 = UpdateFileUtils.sha256Hex(params.targetFile); - + artifactSha256 = archiveSha256(); + + // The bundle's digest (for the install record) is taken from the + // bytes as they are extracted, instead of re-reading the bundle right + // after. Any entry resolving to the bundle path counts; the last write + // wins on disk and in the digest alike. + File bundleTarget = new File(work, "index.bundlejs").getCanonicalFile(); + stagedBundleSha256 = null; try (SafeZipFile zipFile = new SafeZipFile(params.targetFile)) { ensureArchiveWithinLimits(params.targetFile, zipFile, work); Enumeration entries = zipFile.entries(); while (entries.hasMoreElements()) { - zipFile.unzipToPath(entries.nextElement(), work); + ZipEntry entry = entries.nextElement(); + if (!entry.isDirectory() + && new File(work, entry.getName()).getCanonicalFile().equals(bundleTarget)) { + MessageDigest digest = UpdateFileUtils.newSha256(); + zipFile.unzipToPath(entry, work, digest); + stagedBundleSha256 = UpdateFileUtils.toHex(digest.digest()); + } else { + zipFile.unzipToPath(entry, work); + } } } deleteConsumedArchive(); } + private String archiveSha256() throws IOException { + return streamedArchiveSha256 != null + ? streamedArchiveSha256 : UpdateFileUtils.sha256Hex(params.targetFile); + } + /** * Last step of a successful patch task: the completion record (with the * final bundle's digest) goes into the staging directory, which is then @@ -596,7 +637,8 @@ private void promoteStaging() throws IOException, JSONException { if (!bundle.isFile()) { throw new IOException("bundle missing after install: " + bundle); } - String bundleSha256 = UpdateFileUtils.sha256Hex(bundle); + String bundleSha256 = stagedBundleSha256 != null + ? stagedBundleSha256 : UpdateFileUtils.sha256Hex(bundle); InstallRecord.write( work, InstallRecord.build(params.hash, bundleSha256, artifactSha256)); if (params.unzipDirectory.exists()) { @@ -643,7 +685,7 @@ private void deleteConsumedArchive() { private void doPatchFromApk() throws IOException, JSONException { downloadFile(); File work = stagingDirectory(); - artifactSha256 = UpdateFileUtils.sha256Hex(params.targetFile); + artifactSha256 = archiveSha256(); PatchArchiveContents contents = extractPatchArchive(params.targetFile, work); buildArchivePlan( @@ -689,7 +731,7 @@ private void doPatchFromApk() throws IOException, JSONException { private void doPatchFromPpk() throws IOException, JSONException { downloadFile(); File work = stagingDirectory(); - artifactSha256 = UpdateFileUtils.sha256Hex(params.targetFile); + artifactSha256 = archiveSha256(); PatchArchiveContents contents = extractPatchArchive(params.targetFile, work); ArchivePatchPlanResult plan = buildArchivePlan( @@ -716,20 +758,32 @@ private void doPatchFromPpk() throws IOException, JSONException { deleteConsumedArchive(); } + // Dot-named, so the selection under the versions root never picks it. + static final String TRASH_DIR_NAME = ".trash"; + private void doCleanUp() { + final File trash = new File(params.unzipDirectory, TRASH_DIR_NAME); + // Phase 1, under the version-files lock: only renames doomed entries + // into the trash, so activation is never blocked behind a recursive + // delete of several version directories. UpdateContext.getInstance(context).runCleanupWithLatestState( new UpdateContext.CleanupAction() { @Override - public void run(String keepCurrent, String keepPrevious) { + public void run(String[] keepNames) { cleanupOldEntries( params.unzipDirectory.getAbsolutePath(), - keepCurrent, - keepPrevious, - params.maxAgeDays + keepNames, + params.maxAgeDays, + trash.getAbsolutePath() ); } } ); + // Phase 2, lock released: nothing in the trash is reachable as a + // version any more. Also purges leftovers of an interrupted run. + if (trash.isDirectory()) { + cleanupOldEntries(trash.getAbsolutePath(), new String[0], 0, null); + } } private void cleanUpAfterFailure(int taskType) { @@ -931,11 +985,13 @@ private static native void applyDeltaFromSource( String hbcTransformMeta ); + // trashDir non-empty: move the selected entries there instead of + // deleting them (see doCleanUp). private static native void cleanupOldEntries( String rootDir, - String keepCurrent, - String keepPrevious, - int maxAgeDays + String[] keepNames, + int maxAgeDays, + String trashDir ); private static native ArchivePatchPlanResult buildArchivePlan( diff --git a/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java b/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java index 74f70916..94547493 100644 --- a/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +++ b/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java @@ -22,6 +22,7 @@ final class ErrorCodes { static final String UNSUPPORTED_PLATFORM = "UNSUPPORTED_PLATFORM"; static final String APK_INSTALL_PERMISSION_REQUIRED = "APK_INSTALL_PERMISSION_REQUIRED"; static final String APK_INSTALL_FAILED = "APK_INSTALL_FAILED"; + static final String NATIVE_CONFIG_CONFLICT = "NATIVE_CONFIG_CONFLICT"; private ErrorCodes() { } diff --git a/android/src/main/java/cn/reactnative/modules/update/HttpClients.java b/android/src/main/java/cn/reactnative/modules/update/HttpClients.java new file mode 100644 index 00000000..0c935bcf --- /dev/null +++ b/android/src/main/java/cn/reactnative/modules/update/HttpClients.java @@ -0,0 +1,21 @@ +package cn.reactnative.modules.update; + +import okhttp3.OkHttpClient; + +/** + * The one OkHttpClient the SDK owns. Callers derive their timeout variants + * with newBuilder(), which shares the connection pool and dispatcher threads + * instead of each client growing its own. + */ +final class HttpClients { + private HttpClients() { + } + + private static final class Holder { + static final OkHttpClient BASE = new OkHttpClient(); + } + + static OkHttpClient base() { + return Holder.BASE; + } +} diff --git a/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java index 38123909..e7e92178 100644 --- a/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +++ b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java @@ -43,16 +43,27 @@ public Thread newThread(Runnable runnable) { } }); + // Set once the host called configure() in this process. JS config writes + // (syncNativeConfig) are then rejected with NATIVE_CONFIG_CONFLICT. + private static volatile boolean hostConfigured; + + static boolean isHostConfigured() { + return hostConfigured; + } + /** * 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/prepareBundle. Unless JS - * uses nativeConfigSource: 'native', later JS config writes can replace it. + * Await the callback before continuing startup/prepareBundle. The host + * then owns the configuration for this process: the JS client must use + * nativeConfigSource: 'native', a JS config write is rejected with + * NATIVE_CONFIG_CONFLICT. */ public static void configure(Context context, JSONObject options, final ConfigurationCallback callback) { if (context == null || options == null || callback == null) { throw new IllegalArgumentException("context, options and callback are required"); } + hostConfigured = true; final Context applicationContext = context.getApplicationContext(); // Snapshot caller-owned JSON before dispatch, not minutes later on a worker. final String snapshot = options.toString(); diff --git a/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java b/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java index a47a32cc..a6ff9906 100644 --- a/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java +++ b/android/src/main/java/cn/reactnative/modules/update/SafeZipFile.java @@ -1,11 +1,12 @@ package cn.reactnative.modules.update; -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.OutputStream; +import java.security.DigestOutputStream; +import java.security.MessageDigest; import java.util.Enumeration; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; @@ -17,7 +18,7 @@ public SafeZipFile(File file) throws IOException { super(file); } - private static final int BUFFER_SIZE = 8192; + private static final int BUFFER_SIZE = 64 * 1024; // Bytes written by unzipToFile across the archive: the sum of the central // directory's declared sizes is checked up front (inspect), the actual @@ -124,6 +125,11 @@ public ZipEntry nextElement() { } public void unzipToPath(ZipEntry ze, File targetPath) throws IOException { + unzipToPath(ze, targetPath, null); + } + + /** As above; a non-null digest is fed every byte written for the entry. */ + void unzipToPath(ZipEntry ze, File targetPath, MessageDigest digest) throws IOException { String name = ze.getName(); File target = new File(targetPath, name); @@ -141,10 +147,14 @@ public void unzipToPath(ZipEntry ze, File targetPath) throws IOException { target.mkdirs(); return; } - unzipToFile(ze, target); + unzipToFile(ze, target, digest); } public void unzipToFile(ZipEntry ze, File target) throws IOException { + unzipToFile(ze, target, null); + } + + private void unzipToFile(ZipEntry ze, File target, MessageDigest digest) throws IOException { File parent = target.getParentFile(); if (parent != null && !parent.exists() && !parent.mkdirs() && !parent.exists()) { throw new IOException("Failed to create parent dir for " + target); @@ -157,9 +167,12 @@ public void unzipToFile(ZipEntry ze, File target) throws IOException { ? Math.min(declared, ArchiveLimits.MAX_ENTRY_BYTES) : ArchiveLimits.MAX_ENTRY_BYTES; long written = 0; - try (InputStream inputStream = getInputStream(ze)) { - try (BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(target)); - BufferedInputStream input = new BufferedInputStream(inputStream)) { + // One 64KB buffer, no extra buffered wrappers: every read/write is + // already a large block. + try (InputStream input = getInputStream(ze)) { + try (OutputStream output = digest == null + ? new FileOutputStream(target) + : new DigestOutputStream(new FileOutputStream(target), digest)) { byte[] buffer = new byte[BUFFER_SIZE]; int n; while ((n = input.read(buffer, 0, BUFFER_SIZE)) >= 0) { diff --git a/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java b/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java index 29cbfd10..e6ef1d99 100644 --- a/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java +++ b/android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java @@ -37,7 +37,8 @@ final class SyncCoordinator { static final String KEY_CONFIG = "nativeConfig"; // Raw response cache for the JS side to reuse (§10.3), scoped to the - // exact logical request and native config that produced it. + // exact logical request and native config that produced it. Stored in + // its own SharedPreferences file (UpdateContext.getNativeCheckCache). static final String KEY_RESP_CACHE = "nativeCheckResp"; // Set when a round starts, cleared when it ends (§11.4). Residue on the // next launch means the previous process died mid-round (a crash rescue @@ -91,28 +92,19 @@ static BundlePreparationResult prepareBundle(UpdateContext context) throws Inter if (!nativeReady || sContext != context || !context.getIsUsingBundleUrl()) { return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_initialized"); } - String configJson = context.getKv(KEY_CONFIG); - if (configJson == null || configJson.isEmpty()) { - return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_configured"); - } - try { - JSONObject config = new JSONObject(configJson); - if (config.optBoolean("disabled", false)) { - return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled"); - } - if (config.optString("appKey", "").isEmpty()) { - return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config"); - } - } catch (JSONException e) { - return BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config"); + RunnableConfig parsed = parseRunnableConfig(context.getKv(KEY_CONFIG)); + if (parsed.rejection != null) { + return parsed.rejection; } + String configJson = parsed.json; startRound(0); if (!roundStarted.get()) { return BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "config_changed"); } roundDone.await(); if (roundConfigGeneration != UpdateContext.getNativeConfigGeneration() - || !configJson.equals(roundConfigJson) || !configJson.equals(context.getKv(KEY_CONFIG))) { + || !ConfigEquivalence.equivalent(configJson, roundConfigJson) + || !ConfigEquivalence.equivalent(configJson, context.getKv(KEY_CONFIG))) { return BundlePreparationResult.of(BundlePreparationResult.CANCELLED, "config_changed"); } if (roundGeneration != UpdateContext.getResetGeneration()) { @@ -127,14 +119,66 @@ static void recordJsRound(String config) { } /** - * True when JS already obtained a valid response in this process for the - * exact config the native round would use: the delayed round is then a - * duplicate request. Only the scheduled round consults this — the - * crash-rescue path still runs, JS is dead by then. + * True when JS already obtained a valid response in this process for a + * config equivalent (ConfigEquivalence, the cross-platform contract) to + * the one the native round would use: the delayed round is then a + * duplicate request. JS and a host may serialize the same config + * differently, so exact string equality would almost never match. Only + * the scheduled round consults this — the crash-rescue path still runs, + * JS is dead by then. */ private static boolean hasJsRound(UpdateContext context) { String jsConfig = sJsCompletedConfig; - return jsConfig != null && jsConfig.equals(context.getKv(KEY_CONFIG)); + return jsConfig != null + && ConfigEquivalence.equivalent(jsConfig, context.getKv(KEY_CONFIG)); + } + + /** + * The persisted config, parsed and checked once for every entry point + * (prepareBundle, the automatic round's gate, the round itself), so they + * cannot disagree on what is runnable. Exactly one of {config, rejection} + * is set. + */ + static final class RunnableConfig { + @Nullable final String json; + @Nullable final JSONObject config; + // Trimmed, non-empty. + @Nullable final String appKey; + @Nullable final BundlePreparationResult rejection; + + private RunnableConfig(String json, JSONObject config, String appKey, + BundlePreparationResult rejection) { + this.json = json; + this.config = config; + this.appKey = appKey; + this.rejection = rejection; + } + + static RunnableConfig rejected(String status, String reason) { + return new RunnableConfig( + null, null, null, BundlePreparationResult.of(status, reason)); + } + } + + static RunnableConfig parseRunnableConfig(@Nullable String json) { + if (json == null || json.isEmpty()) { + return RunnableConfig.rejected(BundlePreparationResult.SKIPPED, "not_configured"); + } + JSONObject config; + try { + config = new JSONObject(json); + } catch (JSONException e) { + return RunnableConfig.rejected(BundlePreparationResult.FAILED, "invalid_config"); + } + if (config.optBoolean("disabled", false)) { + return RunnableConfig.rejected(BundlePreparationResult.SKIPPED, "disabled"); + } + Object appKey = config.opt("appKey"); + String trimmed = appKey instanceof String ? ((String) appKey).trim() : ""; + if (trimmed.isEmpty()) { + return RunnableConfig.rejected(BundlePreparationResult.FAILED, "invalid_config"); + } + return new RunnableConfig(json, config, trimmed, null); } private SyncCoordinator() { @@ -194,21 +238,8 @@ static boolean isRoundInFlight() { * and download phase to the remaining budget. */ private static boolean hasRunnableConfig(UpdateContext context) { - if (context == null) { - return false; - } - try { - String json = context.getKv(KEY_CONFIG); - if (json == null) { - return false; - } - JSONObject config = new JSONObject(json); - return !config.optBoolean("disabled", false) - && config.opt("appKey") instanceof String - && !config.getString("appKey").trim().isEmpty(); - } catch (JSONException e) { - return false; - } + return context != null + && parseRunnableConfig(context.getKv(KEY_CONFIG)).rejection == null; } static void onConfigured(UpdateContext context) { @@ -310,26 +341,13 @@ private static void runOnce( roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "check_failed"); String configJson = context.getKv(KEY_CONFIG); roundConfigJson = configJson; - if (configJson == null || configJson.isEmpty()) { - roundResult = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "not_configured"); - return; - } - JSONObject config; - try { - config = new JSONObject(configJson); - } catch (JSONException e) { - roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config"); - return; - } - if (config.optBoolean("disabled", false)) { - roundResult = BundlePreparationResult.of(BundlePreparationResult.SKIPPED, "disabled"); - return; - } - String appKey = config.optString("appKey", ""); - if (appKey.isEmpty()) { - roundResult = BundlePreparationResult.of(BundlePreparationResult.FAILED, "invalid_config"); + RunnableConfig parsed = parseRunnableConfig(configJson); + if (parsed.rejection != null) { + roundResult = parsed.rejection; return; } + JSONObject config = parsed.config; + String appKey = parsed.appKey; // Keep the existing interrupted-round breadcrumb and reset generation. try { context.setKv(KEY_ROUND_INCOMPLETE, "1"); @@ -340,8 +358,13 @@ private static void runOnce( context, launchRolledBackVersion, deadlineNanos, resetGeneration, configJson, config, appKey); } finally { + // A committed round already cleared the breadcrumb in its own + // commit (UpdateContext.commitSyncResult); only rounds that ended + // without committing need the extra write. try { - context.removeKv(KEY_ROUND_INCOMPLETE); + if (context.getKv(KEY_ROUND_INCOMPLETE) != null) { + context.removeKv(KEY_ROUND_INCOMPLETE); + } } catch (IllegalStateException ignored) { } } @@ -539,7 +562,7 @@ private static String buildResponseCacheJson( return cacheEntry.toString(); } - private static final OkHttpClient httpClient = new OkHttpClient.Builder() + private static final OkHttpClient httpClient = HttpClients.base().newBuilder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .callTimeout(15, TimeUnit.SECONDS) 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 7c7118ac..96979d26 100644 --- a/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java +++ b/android/src/main/java/cn/reactnative/modules/update/UpdateContext.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.io.InputStream; import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.ThreadFactory; @@ -29,6 +28,10 @@ public class UpdateContext { private final File rootDir; private final Executor executor; private final SharedPreferences sp; + // The native check's raw response cache (SyncCoordinator.KEY_RESP_CACHE) + // lives in its own file: it is a whole server response, and keeping it in + // sp made every small state commit rewrite it. + private final SharedPreferences checkCacheSp; // Our own PackageInfo, looked up once: every getPackageInfo call is a // Binder IPC, and the constructor, the bundle-hash cache and the native // check all need the version name / install time. @@ -48,6 +51,16 @@ public class UpdateContext { // running app has not loaded yet. Volatile: written during launch, read // from the state serial and download executors. private volatile String launchVersion; + // bundleSha256 of launchVersion's install record, read once at launch + // resolution so getConstants does not hit the file system on the JS + // thread. {hash, sha256}; null until a version was resolved. + private volatile String[] launchBundleSha256; + // bundleHash of the embedded bundle, computed once per process on its own + // thread (see startBundleHashComputation); null until known. + private final Object bundleHashLock = new Object(); + private String bundleHash; + private java.util.ArrayList pendingBundleHashListeners = + new java.util.ArrayList<>(); private static final int STATE_OP_SWITCH_VERSION = 1; private static final int STATE_OP_MARK_SUCCESS = 2; private static final int STATE_OP_ROLLBACK = 3; @@ -114,6 +127,8 @@ public Thread newThread(Runnable r) { } this.sp = this.context.getSharedPreferences(Texts.reveal("2f07f0d0ba8e"), Context.MODE_PRIVATE); + this.checkCacheSp = this.context.getSharedPreferences( + Texts.reveal("2f07f0d0ba8e") + "_check", Context.MODE_PRIVATE); this.packageInfo = lookupPackageInfo(this.context); this.reactInstanceManager = pendingReactInstanceManager; @@ -132,6 +147,23 @@ public Thread newThread(Runnable r) { editor.clear(); applyState(editor, nextState); persistEditor(editor, "sync state with binary version"); + clearNativeCheckCache(); + } else { + migrateNativeCheckCache(); + } + startBundleHashComputation(); + } + + // One-time move of the response cache out of the main state file. + private void migrateNativeCheckCache() { + String legacy = sp.getString(SyncCoordinator.KEY_RESP_CACHE, null); + if (legacy == null) { + return; + } + if (checkCacheSp.edit().putString(SyncCoordinator.KEY_RESP_CACHE, legacy).commit()) { + SharedPreferences.Editor editor = sp.edit(); + editor.remove(SyncCoordinator.KEY_RESP_CACHE); + persistEditor(editor, "migrate check cache"); } } @@ -177,32 +209,92 @@ public interface BundleHashListener { * identity of the binary itself, not of whatever hot update is currently * running. Hashes exactly the bytes pdiff patches from: the hardcoded * "index.android.bundle" asset read via AssetManager, same as - * DownloadTask.copyBundledAssetToFile. Runs on the download executor; - * never fails — an empty string means "unknown" and the server falls back - * to the buildTime heuristic. + * DownloadTask.copyBundledAssetToFile. Never fails — an empty string + * means "unknown" and the server falls back to the buildTime heuristic. + * + * Computed once per process on a dedicated thread started by the + * constructor (not on the download executor, where it used to queue + * behind cleanup and downloads and make the first JS checkUpdate send an + * empty hash). The listener runs on that thread, or on the caller's when + * the value is already known. * - * Deliberately java.security.MessageDigest instead of the C++ - * pushy::digest: librnpushy.so is a prebuilt artifact and this must not - * force a rebuild. The NIST vectors in the patch_core tests anchor both - * implementations to the same standard. + * java.security.MessageDigest rather than the C++ pushy::digest: the + * bytes come from an AssetManager stream on the Java side, and the + * platform digest is native-backed already. The NIST vectors in the + * patch_core tests anchor both implementations to the same standard. */ public void getBundleHash(final BundleHashListener listener) { + String known; + synchronized (bundleHashLock) { + known = bundleHash; + if (known == null) { + pendingBundleHashListeners.add(listener); + return; + } + } + listener.onBundleHash(known); + } + + // Package-private: also the native cold-start check's request input + // (SyncCoordinator). Blocks until the shared computation finished — call + // off the main thread. + String computeBundleHash() { + synchronized (bundleHashLock) { + while (bundleHash == null) { + try { + bundleHashLock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return ""; + } + } + return bundleHash; + } + } + + private void startBundleHashComputation() { if (DEBUG) { // Metro serves the bundle in debug; mirror the dev buildTime behaviour. - listener.onBundleHash(""); + publishBundleHash(""); return; } - executor.execute(new Runnable() { + Thread thread = new Thread(new Runnable() { @Override public void run() { - listener.onBundleHash(computeBundleHash()); + String hash = ""; + try { + hash = loadOrHashBundledAsset(); + } catch (Throwable e) { + Log.w(TAG, "bundle hash failed: " + e); + } finally { + publishBundleHash(hash); + } } - }); + }, "pushy-bundle-hash"); + thread.setDaemon(true); + thread.start(); } - // Package-private: also the native cold-start check's request input - // (SyncCoordinator). Blocking — call off the main thread. - String computeBundleHash() { + private void publishBundleHash(String hash) { + java.util.ArrayList listeners; + synchronized (bundleHashLock) { + bundleHash = hash; + listeners = pendingBundleHashListeners; + pendingBundleHashListeners = null; + bundleHashLock.notifyAll(); + } + if (listeners != null) { + for (BundleHashListener listener : listeners) { + try { + listener.onBundleHash(hash); + } catch (Throwable e) { + Log.w(TAG, "bundle hash listener failed: " + e); + } + } + } + } + + private String loadOrHashBundledAsset() { String cachePrefix = getPackageVersion() + "|" + getPackageLastUpdateTime() + "|"; String cached = sp.getString(KEY_BUNDLE_HASH_CACHE, null); if (cached != null && cached.startsWith(cachePrefix)) { @@ -223,20 +315,14 @@ private long getPackageLastUpdateTime() { private String sha256OfBundledAsset(String assetName) { try (InputStream in = context.getAssets().open(assetName)) { - MessageDigest md = MessageDigest.getInstance("SHA-256"); + MessageDigest md = UpdateFileUtils.newSha256(); byte[] buffer = new byte[64 * 1024]; int read; while ((read = in.read(buffer)) != -1) { md.update(buffer, 0, read); } - byte[] digest = md.digest(); - StringBuilder hex = new StringBuilder(digest.length * 2); - for (byte b : digest) { - hex.append(Character.forDigit((b >> 4) & 0xf, 16)); - hex.append(Character.forDigit(b & 0xf, 16)); - } - return hex.toString(); - } catch (IOException | NoSuchAlgorithmException e) { + return UpdateFileUtils.toHex(md.digest()); + } catch (IOException e) { // Expected when there is no embedded bundle (custom name, no // release bundle); "unknown" is a valid answer, not an error. Log.i(TAG, "Cannot hash bundled asset " + assetName + ": " + e); @@ -406,6 +492,16 @@ private boolean persistEditor(SharedPreferences.Editor editor, String reason) { return true; } + /** + * Asynchronous variant for the getConstants path (JS thread): the + * in-memory state changes immediately, the disk write happens on the + * SharedPreferences writer. Losing it to a crash only re-reports the + * one-shot markers on the next launch. + */ + private static void applyEditor(SharedPreferences.Editor editor) { + editor.apply(); + } + /** * Persistence for operations whose result is promised to a caller * (switchVersion / markSuccess / setLocalHashInfo / the cold-start round): @@ -518,9 +614,9 @@ static final class LaunchMarkers { /** * Reads and clears the first-load marker and the rollback mark in one - * commit. getConstants runs this synchronously on the JS thread (a - * TurboModule constant read), so the two writes are folded into a single - * editor; persistence is best-effort on this launch path. + * editor. getConstants runs this synchronously on the JS thread (a + * TurboModule constant read), so the write is applied asynchronously; + * persistence is best-effort on this launch path. */ LaunchMarkers consumeLaunchMarkers() { LaunchMarkers markers; @@ -543,7 +639,7 @@ LaunchMarkers consumeLaunchMarkers() { ); applyState(editor, nextState); } - persistEditor(editor, "consume launch markers"); + applyEditor(editor); } } if (markers.rolledBackVersion != null) { @@ -606,6 +702,11 @@ private void resetToPackagedBundleLocked() { resetState.firstTime = false; resetState.firstTimeOk = true; String uuid = sp.getString("uuid", null); + // The cached check response still advertises the version this reset + // removes; drop it first so a failure aborts before the state wipe. + if (!clearNativeCheckCache()) { + throw new IllegalStateException("Failed to persist state for reset check cache"); + } SharedPreferences.Editor editor = sp.edit(); editor.clear(); applyState(editor, resetState); @@ -614,8 +715,6 @@ private void resetToPackagedBundleLocked() { } persistEditorOrThrow(editor, "reset to packaged bundle"); ignoreRollback = false; - // editor.clear() above already dropped the cached check response; it - // still advertised the version this reset removed. Log.i(TAG, "Reset to packaged bundle"); DownloadTaskParams params = new DownloadTaskParams(); @@ -721,6 +820,8 @@ public String getBundleUrl(String defaultAssetsUrl) { continue; } launchVersion = currentVersion; + launchBundleSha256 = new String[] { + currentVersion, readBundleSha256(currentVersion)}; syncRolledBackVersion = rolledBackVersion(); return bundleFile.toString(); } @@ -747,15 +848,22 @@ static long getNativeConfigGeneration() { void setNativeConfig(String config) { synchronized (commitLock) { - boolean changed = !config.equals(sp.getString(SyncCoordinator.KEY_CONFIG, null)); + // Canonical comparison (ConfigEquivalence): re-sending the same + // config with another key order or trailing slashes must not + // cancel an in-flight round or drop its cache. + boolean changed = !ConfigEquivalence.equivalent( + config, sp.getString(SyncCoordinator.KEY_CONFIG, null)); SharedPreferences.Editor editor = sp.edit(); if (changed) { // Also invalidate on a failed persistence attempt: an older // round must not commit over uncertain configuration state. resetGeneration.incrementAndGet(); nativeConfigGeneration.incrementAndGet(); - editor.remove(SyncCoordinator.KEY_RESP_CACHE); SyncCoordinator.recordJsRound(null); + if (!clearNativeCheckCache()) { + throw new IllegalStateException( + "Failed to persist state for configuration check cache"); + } } // A native-only first launch needs a stable gray-release identity // before JS initializes. Never replace an existing installation ID. @@ -820,13 +928,20 @@ private boolean commitSyncResultState( if (switching) { applyState(editor, computeSwitchState(hash)); } - if (responseCacheJson != null) { - editor.putString(SyncCoordinator.KEY_RESP_CACHE, responseCacheJson); - } + // The round is over once its result lands: clear the + // interrupted-round breadcrumb in the same commit. + editor.remove(SyncCoordinator.KEY_ROUND_INCOMPLETE); persistEditorOrThrow(editor, "commit sync result"); if (switching) { ignoreRollback = false; } + // Advisory cache, separate file: written after the state it + // describes, still under commitLock so a reset cannot interleave. + if (responseCacheJson != null + && !checkCacheSp.edit() + .putString(SyncCoordinator.KEY_RESP_CACHE, responseCacheJson).commit()) { + Log.w(TAG, "Failed to persist native sync response cache"); + } return true; } } @@ -837,6 +952,14 @@ private boolean commitSyncResultState( * unreadable record. Exposed to JS for crash-report attribution. */ String currentBundleSha256(String hash) { + String[] cached = launchBundleSha256; + if (cached != null && cached[0].equals(hash)) { + return cached[1]; + } + return readBundleSha256(hash); + } + + private String readBundleSha256(String hash) { if (hash == null || hash.isEmpty() || !UpdateFileUtils.isSafePathComponent(hash)) { return ""; } @@ -844,6 +967,23 @@ String currentBundleSha256(String hash) { return record == null ? "" : record.optString("bundleSha256", ""); } + /** Raw native check response for JS (§10.3); null when absent. */ + @Nullable + String getNativeCheckCache() { + return checkCacheSp.getString(SyncCoordinator.KEY_RESP_CACHE, null); + } + + private boolean clearNativeCheckCache() { + if (!checkCacheSp.contains(SyncCoordinator.KEY_RESP_CACHE)) { + return true; + } + if (!checkCacheSp.edit().remove(SyncCoordinator.KEY_RESP_CACHE).commit()) { + Log.e(TAG, "Failed to clear native sync response cache"); + return false; + } + return true; + } + boolean hasCompletedVersion(String hash) { if (!UpdateFileUtils.isSafePathComponent(hash)) { return false; @@ -884,10 +1024,15 @@ static String runningVersion() { } interface CleanupAction { - void run(@Nullable String keepCurrent, @Nullable String keepPrevious); + void run(String[] keepNames); } - /** Resolve keep names immediately before deletion, never when enqueued. */ + /** + * Resolve keep names immediately before the cleanup acts, never when + * enqueued. The action runs under versionFilesLock, so it must stay cheap + * (DownloadTask only renames doomed entries into a trash directory and + * deletes them after this returns). + */ void runCleanupWithLatestState(CleanupAction action) { synchronized (versionFilesLock) { final String[] keepNames; @@ -898,13 +1043,7 @@ void runCleanupWithLatestState(CleanupAction action) { launchVersion ); } - if (keepNames == null) { - // JNI has only two keep slots; three distinct live versions are - // safer to defer until the next process than to delete one. - Log.i(TAG, "Skipping cleanup: three live versions need protection"); - return; - } - action.run(keepNames[0], keepNames[1]); + action.run(keepNames); } } diff --git a/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java b/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java index 936f28cc..f285b1a4 100644 --- a/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java +++ b/android/src/main/java/cn/reactnative/modules/update/UpdateFileUtils.java @@ -7,7 +7,7 @@ import java.io.InputStream; final class UpdateFileUtils { - private static final int BUFFER_SIZE = 8192; + private static final int BUFFER_SIZE = 64 * 1024; private UpdateFileUtils() { } @@ -57,27 +57,41 @@ static void removeDirectory(File file) throws IOException { } } - /** Streaming SHA-256 of a file, lowercase hex. */ - static String sha256Hex(File file) throws IOException { + static java.security.MessageDigest newSha256() throws IOException { + try { + return java.security.MessageDigest.getInstance("SHA-256"); + } catch (java.security.NoSuchAlgorithmException e) { + throw new IOException("SHA-256 unavailable", e); + } + } + + /** Feeds the whole file into digest (streaming, bounded memory). */ + static void updateDigest(java.security.MessageDigest digest, File file) throws IOException { try (InputStream in = new FileInputStream(file)) { - java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256"); - byte[] buffer = new byte[64 * 1024]; + byte[] buffer = new byte[BUFFER_SIZE]; int read; while ((read = in.read(buffer)) != -1) { - md.update(buffer, 0, read); + digest.update(buffer, 0, read); } - byte[] digest = md.digest(); - StringBuilder hex = new StringBuilder(digest.length * 2); - for (byte b : digest) { - hex.append(Character.forDigit((b >> 4) & 0xf, 16)); - hex.append(Character.forDigit(b & 0xf, 16)); - } - return hex.toString(); - } catch (java.security.NoSuchAlgorithmException e) { - throw new IOException("SHA-256 unavailable", e); } } + static String toHex(byte[] digest) { + StringBuilder hex = new StringBuilder(digest.length * 2); + for (byte b : digest) { + hex.append(Character.forDigit((b >> 4) & 0xf, 16)); + hex.append(Character.forDigit(b & 0xf, 16)); + } + return hex.toString(); + } + + /** Streaming SHA-256 of a file, lowercase hex. */ + static String sha256Hex(File file) throws IOException { + java.security.MessageDigest md = newSha256(); + updateDigest(md, file); + return toHex(md.digest()); + } + static void copyFile(File from, File to) throws IOException { ensureParentDirectory(to); try ( diff --git a/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java b/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java index 0c38f882..0c09a282 100644 --- a/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +++ b/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java @@ -45,6 +45,8 @@ public Map getConstants() { String currentVersion = updateContext.getCurrentVersion(); constants.put("currentVersion", currentVersion); constants.put("currentVersionInfo", updateContext.getKv("hash_" + currentVersion)); + // Served from the value cached at launch resolution (no file read on + // the JS thread) when currentVersion is the launched version. constants.put("currentBundleSha256", updateContext.currentBundleSha256(currentVersion)); constants.put("buildTime", updateContext.getBuildTime()); constants.put("isUsingBundleUrl", updateContext.getIsUsingBundleUrl()); @@ -284,7 +286,7 @@ public void run() { } public void getBundleHash(final Promise promise) { - // Threading lives in UpdateContext (download executor); resolve is + // Threading lives in UpdateContext (its bundle-hash thread); resolve is // thread-safe. Never rejects — empty string means "unknown". updateContext.getBundleHash(new UpdateContext.BundleHashListener() { @Override @@ -310,7 +312,7 @@ public void run() { * rejects. */ public void getNativeCheckCache(final Promise promise) { - String cached = updateContext.getKv(SyncCoordinator.KEY_RESP_CACHE); + String cached = updateContext.getNativeCheckCache(); promise.resolve(cached == null ? "" : cached); } @@ -333,6 +335,16 @@ public void syncNativeConfig(final String config, final Promise promise) { if (requireNonEmpty(config, "config", promise) == null) { return; } + if (PushyRuntime.isHostConfigured()) { + // The host owns the configuration in this process: a JS write + // would silently replace it. Bad integration fails loudly. + promise.reject(ErrorCodes.NATIVE_CONFIG_CONFLICT, + "[" + ErrorCodes.NATIVE_CONFIG_CONFLICT + "] the host app configured " + + "native sync with PushyRuntime.configure(); set " + + "nativeConfigSource: 'native' in the JS client instead of " + + "syncing a JS config"); + return; + } try { new JSONObject(config); } catch (JSONException e) { diff --git a/android/src/test/java/cn/reactnative/modules/update/CleanupKeepNamesTest.java b/android/src/test/java/cn/reactnative/modules/update/CleanupKeepNamesTest.java index deb7504e..0da81464 100644 --- a/android/src/test/java/cn/reactnative/modules/update/CleanupKeepNamesTest.java +++ b/android/src/test/java/cn/reactnative/modules/update/CleanupKeepNamesTest.java @@ -1,22 +1,23 @@ package cn.reactnative.modules.update; import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertNull; import org.junit.Test; public class CleanupKeepNamesTest { @Test - public void fillsAvailableSlotsAndDeduplicates() { + public void keepsDistinctNonEmptyNamesInOrder() { assertArrayEquals(new String[] {"current", "running"}, CleanupKeepNames.select("current", null, "running")); assertArrayEquals(new String[] {"same", "running"}, CleanupKeepNames.select("same", "same", "running")); - assertArrayEquals(new String[] {"running", null}, - CleanupKeepNames.select(null, null, "running")); + assertArrayEquals(new String[] {"running"}, + CleanupKeepNames.select(null, "", "running")); + assertArrayEquals(new String[0], CleanupKeepNames.select(null, null, null)); } @Test - public void defersWhenThreeDistinctVersionsNeedProtection() { - assertNull(CleanupKeepNames.select("current", "previous", "running")); + public void protectsThreeDistinctLiveVersions() { + assertArrayEquals(new String[] {"current", "previous", "running"}, + CleanupKeepNames.select("current", "previous", "running")); } } diff --git a/android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java b/android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java new file mode 100644 index 00000000..85246e21 --- /dev/null +++ b/android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java @@ -0,0 +1,57 @@ +package cn.reactnative.modules.update; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import org.junit.Test; + +public class ConfigEquivalenceTest { + @Test + public void ignoresKeyOrder() { + assertTrue(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\",\"endpoints\":[\"https://a\"]}", + "{\"endpoints\":[\"https://a\"],\"appKey\":\"k\"}")); + } + + @Test + public void fillsDefaultsForAbsentKeys() { + assertTrue(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\"}", + "{\"appKey\":\"k\",\"disabled\":false,\"queryUrls\":[],\"rnu\":\"\"," + + "\"rn\":\"\",\"afterDownload\":\"none\"}")); + assertFalse(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\"}", "{\"appKey\":\"k\",\"disabled\":true}")); + assertFalse(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\"}", "{\"appKey\":\"k\",\"rn\":\"0.80.0\"}")); + } + + @Test + public void normalizesUrlListsKeepingFirstOccurrence() { + assertTrue(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\",\"endpoints\":[\"https://a/\",\"https://b\",\"https://a\"]," + + "\"queryUrls\":[\"https://q//\",\"https://q\"]}", + "{\"appKey\":\"k\",\"endpoints\":[\"https://a\",\"https://b\"]," + + "\"queryUrls\":[\"https://q\"]}")); + // Order is meaningful: it is the fallback order. + assertFalse(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\",\"endpoints\":[\"https://a\",\"https://b\"]}", + "{\"appKey\":\"k\",\"endpoints\":[\"https://b\",\"https://a\"]}")); + } + + @Test + public void detectsRealChanges() { + assertFalse(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\"}", "{\"appKey\":\"other\"}")); + assertFalse(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\"}", "{\"appKey\":\"k\",\"packageVersion\":\"1.0\"}")); + assertTrue(ConfigEquivalence.equivalent( + "{\"appKey\":\"k\",\"n\":1}", "{\"appKey\":\"k\",\"n\":1.0}")); + } + + @Test + public void unparseableIsNeverEquivalent() { + assertFalse(ConfigEquivalence.equivalent(null, null)); + assertFalse(ConfigEquivalence.equivalent("{", "{")); + assertFalse(ConfigEquivalence.equivalent("[]", "[]")); + assertFalse(ConfigEquivalence.equivalent("{\"appKey\":\"k\"}", null)); + } +} diff --git a/cpp/patch_core/digest.h b/cpp/patch_core/digest.h index a3732b52..51153db4 100644 --- a/cpp/patch_core/digest.h +++ b/cpp/patch_core/digest.h @@ -15,10 +15,11 @@ namespace digest { // (IO-6). Streaming because Android/Harmony read the bundle from the package // (AssetManager / rawfile) rather than from a plain file. // -// Android intentionally does NOT use this implementation: librnpushy.so is a -// prebuilt artifact, so the Java layer hashes with java.security.MessageDigest -// instead. The NIST vectors in the tests anchor both implementations to the -// same standard. +// Android hashes in Java with java.security.MessageDigest instead (backed by +// the platform's native crypto provider): the bytes come from AssetManager +// streams and download sinks on the Java side, so a JNI round trip per chunk +// would buy nothing. The NIST vectors in the tests anchor both +// implementations to the same standard. class Sha256 { public: Sha256(); diff --git a/cpp/patch_core/error_codes.h b/cpp/patch_core/error_codes.h index 852338f5..430ee561 100644 --- a/cpp/patch_core/error_codes.h +++ b/cpp/patch_core/error_codes.h @@ -50,6 +50,10 @@ constexpr const char* kApkInstallPermissionRequired = "APK_INSTALL_PERMISSION_REQUIRED"; // PackageInstaller could not create, write, commit, or continue an APK session. constexpr const char* kApkInstallFailed = "APK_INSTALL_FAILED"; +// JS tried to write the native-check configuration (syncNativeConfig) in a +// process whose host already configured it natively; JS must declare +// nativeConfigSource: 'native' instead of competing for the config. +constexpr const char* kNativeConfigConflict = "NATIVE_CONFIG_CONFLICT"; } // namespace error_codes } // namespace pushy diff --git a/cpp/patch_core/jni_log.h b/cpp/patch_core/jni_log.h new file mode 100644 index 00000000..d667f698 --- /dev/null +++ b/cpp/patch_core/jni_log.h @@ -0,0 +1,31 @@ +#ifndef PUSHY_PATCH_CORE_JNI_LOG_H_ +#define PUSHY_PATCH_CORE_JNI_LOG_H_ + +#include + +#include "obscured_text.h" + +// Keep the core internal to whichever binary links it (see podspec Core). +#pragma GCC visibility push(hidden) + +namespace pushy { +namespace jni_log { + +// Same logcat tag as the Java layer (Texts.LOG_TAG), stored encoded. +inline const char* Tag() { + static const char* const tag = + text::RevealStatic("2812f5d2bac6664436360afc9ba680694b3301"); + return tag; +} + +} // namespace jni_log +} // namespace pushy + +#pragma GCC visibility pop + +// Error-level logcat line under the SDK's tag. For JNI binding failures that +// would otherwise be swallowed: they must be visible in release builds. +#define PUSHY_JNI_LOGE(...) \ + __android_log_print(ANDROID_LOG_ERROR, ::pushy::jni_log::Tag(), __VA_ARGS__) + +#endif // PUSHY_PATCH_CORE_JNI_LOG_H_ diff --git a/cpp/patch_core/jni_natives.h b/cpp/patch_core/jni_natives.h index eaf6e3fa..cc21aace 100644 --- a/cpp/patch_core/jni_natives.h +++ b/cpp/patch_core/jni_natives.h @@ -9,13 +9,19 @@ namespace pushy { namespace jni_natives { +// Called once from JNI_OnLoad: caches the JNI result classes (global refs), +// their constructors and field IDs. Returns false (after logging what is +// missing) when any lookup failed; the natives that need the missing class +// then throw a RuntimeException instead of defaulting fields. +bool InitResultClassCache(JNIEnv* env); + jint SupportedDiffVersion(JNIEnv*, jclass); jobject SyncStateWithBinaryVersion(JNIEnv* env, jclass, jstring package_version, jstring build_time, jobject state_result); jobject RunStateCore(JNIEnv* env, jclass, jint operation, jobject state_result, jstring string_arg, jboolean flag_a, jboolean flag_b); jobject BuildArchivePlan(JNIEnv* env, jclass, jint patch_type, jobjectArray entry_names, jobjectArray copy_froms, jobjectArray copy_tos, jobjectArray deletes); jobjectArray BuildCopyGroups(JNIEnv* env, jclass, jobjectArray copy_froms, jobjectArray copy_tos); void ApplyDeltaFromSource(JNIEnv* env, jclass, jstring source_root, jstring target_root, jstring origin_bundle_path, jstring bundle_patch_path, jstring bundle_output_path, jstring merge_source_subdir, jboolean enable_merge, jobjectArray copy_froms, jobjectArray copy_tos, jobjectArray deletes, jstring hbc_transform_meta); -void CleanupOldEntries(JNIEnv* env, jclass, jstring root_dir, jstring keep_current, jstring keep_previous, jint max_age_days); +void CleanupOldEntries(JNIEnv* env, jclass, jstring root_dir, jobjectArray keep_names, jint max_age_days, jstring trash_dir); jstring FlowBuildRequestBody(JNIEnv* env, jclass, jstring inputJson); jstring FlowOrderEndpointCandidates(JNIEnv* env, jclass, jstring endpointsJson, jdouble randomSample); jboolean FlowIsValidResponse(JNIEnv* env, jclass, jstring responseText); diff --git a/cpp/patch_core/jni_registration.cpp b/cpp/patch_core/jni_registration.cpp index 09fff307..13bca7c4 100644 --- a/cpp/patch_core/jni_registration.cpp +++ b/cpp/patch_core/jni_registration.cpp @@ -9,6 +9,7 @@ #include #include +#include "jni_log.h" #include "jni_natives.h" #include "obscured_text.h" @@ -47,7 +48,7 @@ const std::vector& NativeClasses() { reinterpret_cast(&pushy::jni_natives::BuildCopyGroups)}, {"3b07e4ddb7af6d49363e3aebd9bea3625f3507e4", "723bfed0b88a274923311bb6e5a7826444205fcdf4daae943d432d07e18c93a9887e5a3655c7c2a4949e3355371df78299937648503c43d9d8ae9a68092f0113fd9887857c422602b9d3d6b880721f210be9c3ee8d8f6a5c3c08b7e5ac82767c153b15ffc9e4bb715056321eade98ba16046320051f7d9bb95207f3d14eacedae1ac585b2f1de98aaebe927e190004ffc3a9833a45773214e4cee385674d277229e3c6b8806c1309081eead8f99f71432d48d7d5ccb2967209663a", reinterpret_cast(&pushy::jni_natives::ApplyDeltaFromSource)}, - {"391bf1d0a09e786a2e3b39f7c2a1996859", "723bfed0b88a274923311bb6e5a7826444205fcdf4daae943d432d07e18c93a9887e5a3655c7c2a4949e3355371df78299937648503c43dc9b99", + {"391bf1d0a09e786a2e3b39f7c2a1996859", "723bfed0b88a274923311bb6e5a7826444205fdad2d1b98373002008e8c4ef8e8e655d3f09b0e189889e6a58791ff1c3adc857554c3216f289e6ba", reinterpret_cast(&pushy::jni_natives::CleanupOldEntries)}, }}, {"3919bbc3ab8a6b512c3e08f0c0b6df60452311edfbc8f780624b2d1de38c86b19560762307efcfa0", // FlowBridge @@ -66,13 +67,17 @@ const std::vector& NativeClasses() { } // Registers each method on its own: a host's R8 build may drop an unused -// class or native method, which must not prevent the rest from binding. +// class or native method, which must not prevent the rest from binding. A +// failure is still logged (release builds too): otherwise the first call of +// the unbound method surfaces as an UnsatisfiedLinkError with no hint why. void RegisterAll(JNIEnv* env) { for (const NativeClass& native_class : NativeClasses()) { const std::string class_name = pushy::text::Reveal(native_class.name); jclass clazz = env->FindClass(class_name.c_str()); if (clazz == nullptr) { env->ExceptionClear(); + PUSHY_JNI_LOGE("JNI binding: class %s not found, %zu native methods unbound", + class_name.c_str(), native_class.methods.size()); continue; } for (const NativeMethod& method : native_class.methods) { @@ -85,6 +90,8 @@ void RegisterAll(JNIEnv* env) { }; if (env->RegisterNatives(clazz, &entry, 1) != JNI_OK) { env->ExceptionClear(); + PUSHY_JNI_LOGE("JNI binding: %s.%s%s could not be registered", + class_name.c_str(), name.c_str(), signature.c_str()); } } env->DeleteLocalRef(clazz); @@ -99,5 +106,8 @@ extern "C" JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) { return JNI_ERR; } RegisterAll(env); + // Result classes and their field IDs are resolved once here; a failure is + // logged inside and makes the affected natives throw instead of guessing. + pushy::jni_natives::InitResultClassCache(env); return JNI_VERSION_1_6; } diff --git a/cpp/patch_core/patch_core.cpp b/cpp/patch_core/patch_core.cpp index 6dc81a7a..a2e7a476 100644 --- a/cpp/patch_core/patch_core.cpp +++ b/cpp/patch_core/patch_core.cpp @@ -727,11 +727,16 @@ Status CleanupOldEntries( max_age_days, now); } -Status CleanupOldEntries( +namespace { + +// Lists the non-dot entries of root_dir that are not kept and are at least +// max_age_days old: the one selection rule shared by delete and trash. +Status SelectOldEntries( const std::string& root_dir, const std::vector& keep_names, int max_age_days, - std::time_t now) { + std::time_t now, + std::vector* out) { DIR* dir = opendir(root_dir.c_str()); if (!dir) { if (errno == ENOENT) { @@ -765,15 +770,61 @@ Status CleanupOldEntries( if (effective_now - st.st_mtime < max_age_seconds) { continue; } + out->push_back(name); + } + + closedir(dir); + return Status::Ok(); +} + +} // namespace - Status remove_status = RemovePathRecursively(entry_path); +Status CleanupOldEntries( + const std::string& root_dir, + const std::vector& keep_names, + int max_age_days, + std::time_t now) { + std::vector doomed; + Status select_status = + SelectOldEntries(root_dir, keep_names, max_age_days, now, &doomed); + if (!select_status) { + return select_status; + } + for (const std::string& name : doomed) { + Status remove_status = RemovePathRecursively(JoinPath(root_dir, name)); if (!remove_status) { - closedir(dir); return remove_status; } } + return Status::Ok(); +} - closedir(dir); +Status MoveOldEntriesToTrash( + const std::string& root_dir, + const std::vector& keep_names, + int max_age_days, + const std::string& trash_dir, + std::time_t now) { + std::vector doomed; + Status select_status = + SelectOldEntries(root_dir, keep_names, max_age_days, now, &doomed); + if (!select_status || doomed.empty()) { + return select_status; + } + Status dir_status = EnsureDirectory(trash_dir); + if (!dir_status) { + return dir_status; + } + for (const std::string& name : doomed) { + std::string target = JoinPath(trash_dir, name); + for (int suffix = 1; PathExists(target); ++suffix) { + target = JoinPath(trash_dir, name + "." + IntToString(suffix)); + } + const std::string source = JoinPath(root_dir, name); + if (rename(source.c_str(), target.c_str()) != 0) { + return MakeErrnoStatus("Failed to move " + source + " to trash"); + } + } return Status::Ok(); } diff --git a/cpp/patch_core/patch_core.h b/cpp/patch_core/patch_core.h index 099b895a..cf69856d 100644 --- a/cpp/patch_core/patch_core.h +++ b/cpp/patch_core/patch_core.h @@ -89,6 +89,20 @@ Status CleanupOldEntries( int max_age_days, std::time_t now = 0); +// Same selection as CleanupOldEntries, but each doomed entry is renamed into +// trash_dir (created if needed; must be on the same file system, e.g. a +// dot-named child of root_dir so the selection itself skips it) instead of +// being deleted. Renames are cheap, so a caller can hold its version-files +// lock across this call and delete trash_dir's contents after releasing it +// (CleanupOldEntries(trash_dir, {}, 0)). A name already present in trash_dir +// (left over by an interrupted earlier delete) gets a numeric suffix. +Status MoveOldEntriesToTrash( + const std::string& root_dir, + const std::vector& keep_names, + int max_age_days, + const std::string& trash_dir, + std::time_t now = 0); + bool IsSafeRelativePath(const std::string& path); namespace internal { diff --git a/cpp/patch_core/patch_core_android.cpp b/cpp/patch_core/patch_core_android.cpp index 4f8be6a2..89233983 100644 --- a/cpp/patch_core/patch_core_android.cpp +++ b/cpp/patch_core/patch_core_android.cpp @@ -77,15 +77,20 @@ void pushy::jni_natives::CleanupOldEntries( JNIEnv* env, jclass, jstring root_dir, - jstring keep_current, - jstring keep_previous, - jint max_age_days) { + jobjectArray keep_names, + jint max_age_days, + jstring trash_dir) { try { - const pushy::delta::Status status = pushy::delta::CleanupOldEntries( - JStringToString(env, root_dir), - JStringToString(env, keep_current), - JStringToString(env, keep_previous), - static_cast(max_age_days)); + const std::string root = JStringToString(env, root_dir); + const std::vector keep = JArrayToVector(env, keep_names); + const std::string trash = JStringToString(env, trash_dir); + // A trash directory turns the delete into renames (cheap enough to run + // under the caller's version-files lock); the caller purges it later. + const pushy::delta::Status status = trash.empty() + ? pushy::delta::CleanupOldEntries( + root, keep, static_cast(max_age_days)) + : pushy::delta::MoveOldEntriesToTrash( + root, keep, static_cast(max_age_days), trash); if (!status.ok) { ThrowRuntimeException(env, status.message); } diff --git a/cpp/patch_core/tests/patch_core_test.cpp b/cpp/patch_core/tests/patch_core_test.cpp index 98823a40..3fafc8b9 100644 --- a/cpp/patch_core/tests/patch_core_test.cpp +++ b/cpp/patch_core/tests/patch_core_test.cpp @@ -25,6 +25,7 @@ namespace { using pushy::delta::ApplyPatchFromFileSource; using pushy::delta::BundleRebuilder; using pushy::delta::CleanupOldEntries; +using pushy::delta::MoveOldEntriesToTrash; using pushy::delta::CopyOperation; using pushy::delta::FileSourcePatchOptions; using pushy::delta::PatchManifest; @@ -617,6 +618,46 @@ void TestCleanupOldEntriesRemovesOnlyExpiredPaths() { Expect(Exists(JoinPath(root, ".hidden")), "hidden entry should be kept"); } +void TestMoveOldEntriesToTrashKeepsEveryLiveVersion() { + TempDir temp; + const std::string root = JoinPath(temp.path, "trash-root"); + const std::string trash = JoinPath(root, ".trash"); + EnsureDirectory(root); + + WriteFile(JoinPath(root, "current/index.bundlejs"), "current"); + WriteFile(JoinPath(root, "previous/index.bundlejs"), "previous"); + WriteFile(JoinPath(root, "running/index.bundlejs"), "running"); + WriteFile(JoinPath(root, "stale/index.bundlejs"), "stale"); + WriteFile(JoinPath(root, "recent/index.bundlejs"), "recent"); + // Leftover of an interrupted earlier delete with the same name. + WriteFile(JoinPath(trash, "stale/index.bundlejs"), "older stale"); + + const std::time_t now = 1'700'000'000; + const std::time_t old_time = now - (9 * 24 * 60 * 60); + for (const char* name : {"current", "previous", "running", "stale"}) { + SetMtime(JoinPath(root, name), old_time); + } + SetMtime(JoinPath(root, "recent"), now - (2 * 24 * 60 * 60)); + + Status status = MoveOldEntriesToTrash( + root, std::vector{"current", "previous", "running"}, 7, + trash, now); + Expect(status.ok, status.message); + Expect(Exists(JoinPath(root, "current")), "current is kept"); + Expect(Exists(JoinPath(root, "previous")), "previous is kept"); + Expect(Exists(JoinPath(root, "running")), "running is kept"); + Expect(Exists(JoinPath(root, "recent")), "recent is kept"); + Expect(!Exists(JoinPath(root, "stale")), "stale leaves the root"); + Expect(Exists(JoinPath(trash, "stale.1/index.bundlejs")), + "stale lands in trash next to the leftover"); + Expect(Exists(JoinPath(trash, "stale/index.bundlejs")), "leftover untouched"); + + Status purge = CleanupOldEntries(trash, std::vector{}, 0); + Expect(purge.ok, purge.message); + Expect(!Exists(JoinPath(trash, "stale")), "trash is purged"); + Expect(!Exists(JoinPath(trash, "stale.1")), "trash is purged"); +} + void TestStateCoreSyncBinaryVersionResetsUpdates() { State state; state.package_version = "1.0.0"; @@ -1097,6 +1138,7 @@ int main(int argc, char** argv) { {"DirectoryWalkersBoundNestingDepth", TestDirectoryWalkersBoundNestingDepth}, {"HpatchRejectsOversizedLzmaDictionary", TestHpatchRejectsOversizedLzmaDictionary}, {"CleanupOldEntriesRemovesOnlyExpiredPaths", TestCleanupOldEntriesRemovesOnlyExpiredPaths}, + {"MoveOldEntriesToTrashKeepsEveryLiveVersion", TestMoveOldEntriesToTrashKeepsEveryLiveVersion}, {"StateCoreSyncBinaryVersionResetsUpdates", TestStateCoreSyncBinaryVersionResetsUpdates}, {"StateCoreSwitchVersionAndMarkSuccess", TestStateCoreSwitchVersionAndMarkSuccess}, {"StateCoreResolveLaunchStateAndRollback", TestStateCoreResolveLaunchStateAndRollback}, diff --git a/cpp/patch_core/update_core_android.cpp b/cpp/patch_core/update_core_android.cpp index aa442865..00a2d684 100644 --- a/cpp/patch_core/update_core_android.cpp +++ b/cpp/patch_core/update_core_android.cpp @@ -1,5 +1,6 @@ #include +#include "jni_log.h" #include "jni_natives.h" #include "obscured_text.h" @@ -20,20 +21,169 @@ using pushy::jni_util::JStringToString; using pushy::jni_util::ThrowRuntimeException; using pushy::state_ops::StateOperation; +constexpr const char* kStringSig = "Ljava/lang/String;"; + +// JNI result classes, resolved once in JNI_OnLoad (InitResultClassCache) +// instead of FindClass + a GetFieldID per field on every state operation. +// Written only before any native method can run, read-only afterwards. +struct StateResultIds { + bool ready = false; + jclass clazz = nullptr; + jmethodID constructor = nullptr; + jfieldID package_version = nullptr; + jfieldID build_time = nullptr; + jfieldID current_version = nullptr; + jfieldID last_version = nullptr; + jfieldID first_time = nullptr; + jfieldID first_time_ok = nullptr; + jfieldID rolled_back_version = nullptr; + jfieldID changed = nullptr; + jfieldID stale_version_to_delete = nullptr; + jfieldID load_version = nullptr; + jfieldID did_rollback = nullptr; + jfieldID consumed_first_time = nullptr; +}; + +struct PlanResultIds { + bool ready = false; + jclass clazz = nullptr; + jmethodID constructor = nullptr; + jfieldID merge_source_subdir = nullptr; + jfieldID enable_merge = nullptr; +}; + +struct CopyGroupIds { + bool ready = false; + jclass clazz = nullptr; + jclass string_class = nullptr; + jmethodID constructor = nullptr; + jfieldID from = nullptr; + jfieldID to_paths = nullptr; +}; + +StateResultIds g_state; +PlanResultIds g_plan; +CopyGroupIds g_copy_group; + +// Resolves a class to a global ref. Logs and clears the pending exception on +// failure. +jclass FindGlobalClass(JNIEnv* env, const std::string& name) { + jclass local = env->FindClass(name.c_str()); + if (local == nullptr) { + env->ExceptionClear(); + PUSHY_JNI_LOGE("JNI result class %s not found", name.c_str()); + return nullptr; + } + auto* global = static_cast(env->NewGlobalRef(local)); + env->DeleteLocalRef(local); + return global; +} + +// A missing field must never silently default: a lost firstTimeOk would read +// as false and roll back every launch. Logs the exact member. +bool LookupField( + JNIEnv* env, + jclass clazz, + const std::string& class_name, + const char* name, + const char* signature, + jfieldID* out) { + *out = env->GetFieldID(clazz, name, signature); + if (*out == nullptr) { + env->ExceptionClear(); + PUSHY_JNI_LOGE("JNI field %s.%s (%s) not found", class_name.c_str(), name, + signature); + return false; + } + return true; +} + +bool LookupConstructor( + JNIEnv* env, jclass clazz, const std::string& class_name, jmethodID* out) { + *out = env->GetMethodID(clazz, "", "()V"); + if (*out == nullptr) { + env->ExceptionClear(); + PUSHY_JNI_LOGE("JNI no-arg constructor of %s not found", class_name.c_str()); + return false; + } + return true; +} + +bool InitStateResultIds(JNIEnv* env) { + const std::string name = pushy::text::Reveal( + "3919bbc3ab8a6b512c3e08f0c0b6df60452311edfbc8f780624b2d1de38c93a99b63511201f9cd97878c695522"); + StateResultIds ids; + ids.clazz = FindGlobalClass(env, name); + if (ids.clazz == nullptr) { + return false; + } + bool ok = LookupConstructor(env, ids.clazz, name, &ids.constructor); + ok &= LookupField(env, ids.clazz, name, "packageVersion", kStringSig, &ids.package_version); + ok &= LookupField(env, ids.clazz, name, "buildTime", kStringSig, &ids.build_time); + ok &= LookupField(env, ids.clazz, name, "currentVersion", kStringSig, &ids.current_version); + ok &= LookupField(env, ids.clazz, name, "lastVersion", kStringSig, &ids.last_version); + ok &= LookupField(env, ids.clazz, name, "firstTime", "Z", &ids.first_time); + ok &= LookupField(env, ids.clazz, name, "firstTimeOk", "Z", &ids.first_time_ok); + ok &= LookupField(env, ids.clazz, name, "rolledBackVersion", kStringSig, &ids.rolled_back_version); + ok &= LookupField(env, ids.clazz, name, "changed", "Z", &ids.changed); + ok &= LookupField(env, ids.clazz, name, "staleVersionToDelete", kStringSig, &ids.stale_version_to_delete); + ok &= LookupField(env, ids.clazz, name, "loadVersion", kStringSig, &ids.load_version); + ok &= LookupField(env, ids.clazz, name, "didRollback", "Z", &ids.did_rollback); + ok &= LookupField(env, ids.clazz, name, "consumedFirstTime", "Z", &ids.consumed_first_time); + ids.ready = ok; + g_state = ids; + return ok; +} + +bool InitPlanResultIds(JNIEnv* env) { + const std::string name = pushy::text::Reveal( + "3919bbc3ab8a6b512c3e08f0c0b6df60452311edfbc8f780624b2d1de38c81af997f5d270bdbc9b181974c55371dc2c8b9926855"); + PlanResultIds ids; + ids.clazz = FindGlobalClass(env, name); + if (ids.clazz == nullptr) { + return false; + } + bool ok = LookupConstructor(env, ids.clazz, name, &ids.constructor); + ok &= LookupField(env, ids.clazz, name, "mergeSourceSubdir", kStringSig, &ids.merge_source_subdir); + ok &= LookupField(env, ids.clazz, name, "enableMerge", "Z", &ids.enable_merge); + ids.ready = ok; + g_plan = ids; + return ok; +} + +bool InitCopyGroupIds(JNIEnv* env) { + const std::string name = pushy::text::Reveal( + "3919bbc3ab8a6b512c3e08f0c0b6df60452311edfbc8f780624b2d1de38c83b28a6e732301fed897878c695522"); + CopyGroupIds ids; + ids.clazz = FindGlobalClass(env, name); + ids.string_class = FindGlobalClass(env, "java/lang/String"); + if (ids.clazz == nullptr || ids.string_class == nullptr) { + return false; + } + bool ok = LookupConstructor(env, ids.clazz, name, &ids.constructor); + ok &= LookupField(env, ids.clazz, name, "from", kStringSig, &ids.from); + ok &= LookupField(env, ids.clazz, name, "toPaths", "[Ljava/lang/String;", &ids.to_paths); + ids.ready = ok; + g_copy_group = ids; + return ok; +} + +// Throws (and returns false) when the class needed by a native is unusable; +// the reason was logged once at load time. +bool RequireReady(JNIEnv* env, bool ready, const char* what) { + if (!ready) { + ThrowRuntimeException( + env, std::string("JNI result class unavailable: ") + what + + " (see the load-time log for the missing member)"); + } + return ready; +} + void SetStringField( JNIEnv* env, jobject target, - jclass target_class, - const char* field_name, + jfieldID field, const std::string& value) { - jfieldID field = - env->GetFieldID(target_class, field_name, "Ljava/lang/String;"); - if (field == nullptr) { - // Clear the pending NoSuchFieldError so subsequent JNI calls are not - // executed with an exception pending (undefined behavior / CheckJNI abort). - env->ExceptionClear(); - return; - } if (value.empty()) { env->SetObjectField(target, field, nullptr); return; @@ -44,31 +194,11 @@ void SetStringField( env->DeleteLocalRef(java_value); } -void SetBooleanField( - JNIEnv* env, - jobject target, - jclass target_class, - const char* field_name, - bool value) { - jfieldID field = env->GetFieldID(target_class, field_name, "Z"); - if (field == nullptr) { - env->ExceptionClear(); - return; - } +void SetBooleanField(JNIEnv* env, jobject target, jfieldID field, bool value) { env->SetBooleanField(target, field, value ? JNI_TRUE : JNI_FALSE); } -std::string GetStringField( - JNIEnv* env, - jobject target, - jclass target_class, - const char* field_name) { - jfieldID field = - env->GetFieldID(target_class, field_name, "Ljava/lang/String;"); - if (field == nullptr) { - env->ExceptionClear(); - return std::string(); - } +std::string GetStringField(JNIEnv* env, jobject target, jfieldID field) { auto* value = static_cast(env->GetObjectField(target, field)); std::string result = JStringToString(env, value); if (value != nullptr) { @@ -77,60 +207,28 @@ std::string GetStringField( return result; } -bool GetBooleanField( - JNIEnv* env, - jobject target, - jclass target_class, - const char* field_name) { - jfieldID field = env->GetFieldID(target_class, field_name, "Z"); - if (field == nullptr) { - env->ExceptionClear(); - return false; - } +bool GetBooleanField(JNIEnv* env, jobject target, jfieldID field) { return env->GetBooleanField(target, field) == JNI_TRUE; } -pushy::state::State ReadState( - const std::string& package_version, - const std::string& build_time, - const std::string& current_version, - const std::string& last_version, - bool first_time, - bool first_time_ok, - const std::string& rolled_back_version) { - pushy::state::State state; - state.package_version = package_version; - state.build_time = build_time; - state.current_version = current_version; - state.last_version = last_version; - state.first_time = first_time; - state.first_time_ok = first_time_ok; - state.rolled_back_version = rolled_back_version; - return state; -} - +// Caller has checked g_state.ready. pushy::state::State ReadStateFromResult(JNIEnv* env, jobject state_result) { + pushy::state::State state; if (state_result == nullptr) { - return pushy::state::State(); - } - - jclass state_class = env->GetObjectClass(state_result); - if (state_class == nullptr) { - return pushy::state::State(); + return state; } - - pushy::state::State state = ReadState( - GetStringField(env, state_result, state_class, "packageVersion"), - GetStringField(env, state_result, state_class, "buildTime"), - GetStringField(env, state_result, state_class, "currentVersion"), - GetStringField(env, state_result, state_class, "lastVersion"), - GetBooleanField(env, state_result, state_class, "firstTime"), - GetBooleanField(env, state_result, state_class, "firstTimeOk"), - GetStringField(env, state_result, state_class, "rolledBackVersion")); - env->DeleteLocalRef(state_class); + state.package_version = GetStringField(env, state_result, g_state.package_version); + state.build_time = GetStringField(env, state_result, g_state.build_time); + state.current_version = GetStringField(env, state_result, g_state.current_version); + state.last_version = GetStringField(env, state_result, g_state.last_version); + state.first_time = GetBooleanField(env, state_result, g_state.first_time); + state.first_time_ok = GetBooleanField(env, state_result, g_state.first_time_ok); + state.rolled_back_version = + GetStringField(env, state_result, g_state.rolled_back_version); return state; } +// Caller has checked g_state.ready. jobject NewStateCoreResult( JNIEnv* env, const pushy::state::State& state, @@ -139,116 +237,67 @@ jobject NewStateCoreResult( const std::string& load_version, bool did_rollback, bool consumed_first_time) { - jclass result_class = - env->FindClass( - pushy::text::Reveal("3919bbc3ab8a6b512c3e08f0c0b6df60452311edfbc8f780624b2d1de38c93a99b63511201f9cd97878c695522").c_str()); - if (result_class == nullptr) { - return nullptr; - } - - jmethodID constructor = env->GetMethodID(result_class, "", "()V"); - if (constructor == nullptr) { - return nullptr; - } - - jobject result = env->NewObject(result_class, constructor); + jobject result = env->NewObject(g_state.clazz, g_state.constructor); if (result == nullptr) { return nullptr; } - SetStringField(env, result, result_class, "packageVersion", state.package_version); - SetStringField(env, result, result_class, "buildTime", state.build_time); - SetStringField(env, result, result_class, "currentVersion", state.current_version); - SetStringField(env, result, result_class, "lastVersion", state.last_version); - SetBooleanField(env, result, result_class, "firstTime", state.first_time); - SetBooleanField(env, result, result_class, "firstTimeOk", state.first_time_ok); + SetStringField(env, result, g_state.package_version, state.package_version); + SetStringField(env, result, g_state.build_time, state.build_time); + SetStringField(env, result, g_state.current_version, state.current_version); + SetStringField(env, result, g_state.last_version, state.last_version); + SetBooleanField(env, result, g_state.first_time, state.first_time); + SetBooleanField(env, result, g_state.first_time_ok, state.first_time_ok); SetStringField( - env, - result, - result_class, - "rolledBackVersion", - state.rolled_back_version); - SetBooleanField(env, result, result_class, "changed", changed); + env, result, g_state.rolled_back_version, state.rolled_back_version); + SetBooleanField(env, result, g_state.changed, changed); SetStringField( - env, - result, - result_class, - "staleVersionToDelete", - stale_version_to_delete); - SetStringField(env, result, result_class, "loadVersion", load_version); - SetBooleanField(env, result, result_class, "didRollback", did_rollback); + env, result, g_state.stale_version_to_delete, stale_version_to_delete); + SetStringField(env, result, g_state.load_version, load_version); + SetBooleanField(env, result, g_state.did_rollback, did_rollback); SetBooleanField( - env, - result, - result_class, - "consumedFirstTime", - consumed_first_time); + env, result, g_state.consumed_first_time, consumed_first_time); return result; } jobject NewArchivePatchPlanResult( JNIEnv* env, const pushy::archive_patch::ArchivePatchPlan& plan) { - jclass result_class = - env->FindClass( - pushy::text::Reveal("3919bbc3ab8a6b512c3e08f0c0b6df60452311edfbc8f780624b2d1de38c81af997f5d270bdbc9b181974c55371dc2c8b9926855").c_str()); - if (result_class == nullptr) { + if (!RequireReady(env, g_plan.ready, "archive plan result")) { return nullptr; } - - jmethodID constructor = env->GetMethodID(result_class, "", "()V"); - if (constructor == nullptr) { - return nullptr; - } - - jobject result = env->NewObject(result_class, constructor); + jobject result = env->NewObject(g_plan.clazz, g_plan.constructor); if (result == nullptr) { return nullptr; } - SetStringField( - env, - result, - result_class, - "mergeSourceSubdir", - plan.merge_source_subdir); - SetBooleanField(env, result, result_class, "enableMerge", plan.enable_merge); + SetStringField(env, result, g_plan.merge_source_subdir, plan.merge_source_subdir); + SetBooleanField(env, result, g_plan.enable_merge, plan.enable_merge); return result; } +// Caller has checked g_copy_group.ready. jobject NewCopyGroupResult( JNIEnv* env, - jclass result_class, - jclass string_class, const pushy::archive_patch::CopyGroup& group) { - jmethodID constructor = env->GetMethodID(result_class, "", "()V"); - if (constructor == nullptr) { - return nullptr; - } - - jobject result = env->NewObject(result_class, constructor); + jobject result = env->NewObject(g_copy_group.clazz, g_copy_group.constructor); if (result == nullptr) { return nullptr; } - SetStringField(env, result, result_class, "from", group.from); - jfieldID to_paths_field = - env->GetFieldID(result_class, "toPaths", "[Ljava/lang/String;"); - if (to_paths_field != nullptr) { - jobjectArray to_paths = env->NewObjectArray( - static_cast(group.to_paths.size()), string_class, nullptr); - if (to_paths != nullptr) { - for (jsize index = 0; index < static_cast(group.to_paths.size()); - ++index) { - jstring value = env->NewStringUTF(group.to_paths[index].c_str()); - env->SetObjectArrayElement(to_paths, index, value); - env->DeleteLocalRef(value); - } - env->SetObjectField(result, to_paths_field, to_paths); - env->DeleteLocalRef(to_paths); + SetStringField(env, result, g_copy_group.from, group.from); + jobjectArray to_paths = env->NewObjectArray( + static_cast(group.to_paths.size()), g_copy_group.string_class, + nullptr); + if (to_paths != nullptr) { + for (jsize index = 0; index < static_cast(group.to_paths.size()); + ++index) { + jstring value = env->NewStringUTF(group.to_paths[index].c_str()); + env->SetObjectArrayElement(to_paths, index, value); + env->DeleteLocalRef(value); } - } else { - env->ExceptionClear(); + env->SetObjectField(result, g_copy_group.to_paths, to_paths); + env->DeleteLocalRef(to_paths); } return result; @@ -296,6 +345,14 @@ jobject MakeStateResult( } // namespace +bool pushy::jni_natives::InitResultClassCache(JNIEnv* env) { + // Every class is attempted so the log names all missing members at once. + bool ok = InitStateResultIds(env); + ok &= InitPlanResultIds(env); + ok &= InitCopyGroupIds(env); + return ok; +} + // 客户端可消费的 diff 轨道版本(能力上报,而非 SDK 版本映射);JS 层经 // getConstants 暴露,随 checkUpdate 以 diffV 上报,服务端据此下发 v2 轨道。 jint pushy::jni_natives::SupportedDiffVersion( @@ -314,6 +371,9 @@ jobject pushy::jni_natives::SyncStateWithBinaryVersion( // allocations) must not unwind through the JNI boundary: convert them to a // Java RuntimeException like every other failure of these entry points. try { + if (!RequireReady(env, g_state.ready, "state result")) { + return nullptr; + } pushy::state::State state = ReadStateFromResult(env, state_result); pushy::state::BinaryVersionSyncResult result = pushy::state::SyncBinaryVersion( state, @@ -337,6 +397,9 @@ jobject pushy::jni_natives::RunStateCore( jboolean flag_a, jboolean flag_b) { try { + if (!RequireReady(env, g_state.ready, "state result")) { + return nullptr; + } const pushy::state::State state = ReadStateFromResult(env, state_result); switch (static_cast(operation)) { case StateOperation::kSwitchVersion: @@ -451,30 +514,20 @@ jobjectArray pushy::jni_natives::BuildCopyGroups( return nullptr; } - jclass result_class = - env->FindClass( - pushy::text::Reveal("3919bbc3ab8a6b512c3e08f0c0b6df60452311edfbc8f780624b2d1de38c83b28a6e732301fed897878c695522").c_str()); - if (result_class == nullptr) { + if (!RequireReady(env, g_copy_group.ready, "copy group result")) { return nullptr; } - - jclass string_class = env->FindClass("java/lang/String"); - if (string_class == nullptr) { - return nullptr; - } - jobjectArray result = env->NewObjectArray( - static_cast(groups.size()), result_class, nullptr); + static_cast(groups.size()), g_copy_group.clazz, nullptr); if (result == nullptr) { return nullptr; } for (jsize index = 0; index < static_cast(groups.size()); ++index) { - jobject group = NewCopyGroupResult(env, result_class, string_class, groups[index]); + jobject group = NewCopyGroupResult(env, groups[index]); env->SetObjectArrayElement(result, index, group); env->DeleteLocalRef(group); } - env->DeleteLocalRef(string_class); return result; } catch (const std::exception& error) { ThrowRuntimeException(env, error.what()); From fd1dfe001f382ebc5fa7c40db69d640e8ac3d6dc Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:24:19 +0800 Subject: [PATCH 08/21] chore: drop dead build/publish leftovers - babel.config.js, @babel/core and @react-native/babel-preset: nothing at the root transpiles with Babel (bun runs the tests, tsc type-checks); every Example app carries its own babel config and preset. - .npmignore: package.json `files` decides what ships and check-packlist.js guards it; npm pack --dry-run lists the same 143 files before and after (only package.json's own size changes). - scripts/verify-android-so.sh: unreferenced wrapper around verify-android-so.js. - scripts/prepublish.ts and its prepublishOnly hook: on CI it rewrote package.json to the tag version (check-release-version.js already fails the job unless they match) and re-ran verify-android-so.js (publish.yml runs it explicitly in both publish jobs). Its local branch only served the also-removed publish:local script; releases go through publish.yml. - nanoid: src is inlining its id generator. generate-flow-vectors.ts takes URL from node:url: with prepublish.ts gone the program's global URL (react-native's declaration) no longer type-checks as a node:fs/node:url path. Co-Authored-By: Claude Opus 5.5 --- .npmignore | 95 ---------------- babel.config.js | 3 - bun.lock | 87 ++++++-------- package.json | 7 -- scripts/generate-flow-vectors.ts | 2 +- scripts/prepublish.ts | 189 ------------------------------- scripts/verify-android-so.sh | 5 - 7 files changed, 33 insertions(+), 355 deletions(-) delete mode 100644 .npmignore delete mode 100644 babel.config.js delete mode 100644 scripts/prepublish.ts delete mode 100755 scripts/verify-android-so.sh diff --git a/.npmignore b/.npmignore deleted file mode 100644 index 69b72780..00000000 --- a/.npmignore +++ /dev/null @@ -1,95 +0,0 @@ -.babelrc -.npmignore -.eslintrc -.nvmrc -.travis.yml -Example -android/build -.vscode -.github/ - -# OSX -# -.DS_Store - -# Xcode -# -build/ -*.pbxuser -!default.pbxuser -*.mode1v3 -!default.mode1v3 -*.mode2v3 -!default.mode2v3 -*.perspectivev3 -!default.perspectivev3 -xcuserdata -*.xccheckout -*.moved-aside -DerivedData -*.hmap -*.ipa -*.xcuserstate -project.xcworkspace - -# Android/IJ -# -.idea -.gradle -local.properties -android/build -android/obj -.project - -# node.js -# -node_modules/ -npm-debug.log -Example -yarn.lock -bun.lock -*.test.ts -*.test.js -src/__tests__/ - -# Native build caches / intermediates (must not ship to consumers) -android/.cxx/ -android/.gradle/ -**/*.o -CODE_AUDIT.md - -domains.json -endpoints.json -endpoints_cresc.json - -tea.yaml - -e2e/ - -harmony/** -!harmony/pushy.har -!harmony/pushy/src/** -!harmony/hvigor-plugin.ts - -.tmp -.cursor -.e2e-rn077-oldarch -.claude -.bun-cache -.ohpm-cache - -android/jni/lzma/bin -android/jni/lzma/DOC - -# Ignore unnecessary development files -scripts/** -harmony/pushy/src/test/ -babel.config.js -tsconfig.json -bunfig.toml - -# Ignore temporary files created by npm pack during CI builds -*.tgz -package - - diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index f7b3da3b..00000000 --- a/babel.config.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - presets: ['module:@react-native/babel-preset'], -}; diff --git a/bun.lock b/bun.lock index 83f37990..c8f5be7c 100644 --- a/bun.lock +++ b/bun.lock @@ -4,13 +4,8 @@ "workspaces": { "": { "name": "react-native-update", - "dependencies": { - "nanoid": "^3.3.3", - }, "devDependencies": { - "@babel/core": "^7.25.8", "@biomejs/biome": "^2.5.5", - "@react-native/babel-preset": "^0.73.21", "@react-native/typescript-config": "0.84.1", "@types/bun": "^1.3.11", "@types/node": "^25.5.0", @@ -23,7 +18,7 @@ }, "peerDependencies": { "react": ">=16.8.0", - "react-native": ">=0.59.0", + "react-native": "*", }, }, }, @@ -556,7 +551,7 @@ "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], - "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], @@ -580,8 +575,6 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="], - "envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="], "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], @@ -660,8 +653,6 @@ "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], @@ -814,9 +805,7 @@ "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], - "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], @@ -948,11 +937,9 @@ "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "scheduler": ["scheduler@0.24.0-canary-efb381bbf-20230505", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA=="], - "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], @@ -1096,30 +1083,42 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@jest/environment/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], "@jest/fake-timers/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], "@jest/types/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], - "@react-native-community/cli/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - - "@react-native-community/cli-doctor/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - "@react-native-community/cli-server-api/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], - "@react-native-community/cli-tools/semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], - - "@react-native/dev-middleware/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "chrome-launcher/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], @@ -1134,12 +1133,6 @@ "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "compression/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "finalhandler/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], @@ -1164,14 +1157,10 @@ "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "logkitty/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], "make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - "metro/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "metro/hermes-parser": ["hermes-parser@0.23.1", "", { "dependencies": { "hermes-estree": "0.23.1" } }, "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA=="], "metro/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1180,8 +1169,6 @@ "metro-babel-transformer/hermes-parser": ["hermes-parser@0.23.1", "", { "dependencies": { "hermes-estree": "0.23.1" } }, "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA=="], - "metro-file-map/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], @@ -1200,12 +1187,12 @@ "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], - "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], @@ -1226,6 +1213,12 @@ "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@babel/helper-define-polyfill-provider/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@jest/environment/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "@jest/fake-timers/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], @@ -1234,20 +1227,12 @@ "@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], - "@react-native/dev-middleware/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "chrome-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "chromium-edge-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "compression/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "jest-environment-node/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], @@ -1260,8 +1245,6 @@ "jest-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "logkitty/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "logkitty/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], @@ -1270,10 +1253,6 @@ "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.23.1", "", {}, "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg=="], - "metro-file-map/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "metro/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "metro/hermes-parser/hermes-estree": ["hermes-estree@0.23.1", "", {}, "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg=="], "pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], @@ -1282,8 +1261,6 @@ "pretty-format/@jest/types/@types/yargs": ["@types/yargs@15.0.19", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA=="], - "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], diff --git a/package.json b/package.json index 394bfb02..ef08020a 100644 --- a/package.json +++ b/package.json @@ -30,8 +30,6 @@ "LICENSE" ], "scripts": { - "prepublishOnly": "NODE_ENV=production bun scripts/prepublish.ts", - "publish:local": "SKIP_NATIVE_BUILD=1 npm publish", "prepack": "bun submodule && bun lint", "lint": "biome check src && tsc --noEmit && node scripts/check-harmony-types.js && node scripts/check-native-spec-parity.js", "lint:harmony": "node scripts/check-harmony-types.js", @@ -89,18 +87,13 @@ "react-native": "*" }, "homepage": "https://github.com/reactnativecn/react-native-update#readme", - "dependencies": { - "nanoid": "^3.3.3" - }, "codegenConfig": { "name": "RCTPushySpec", "type": "modules", "jsSrcsDir": "src" }, "devDependencies": { - "@babel/core": "^7.25.8", "@biomejs/biome": "^2.5.5", - "@react-native/babel-preset": "^0.73.21", "@react-native/typescript-config": "0.84.1", "@types/bun": "^1.3.11", "@types/node": "^25.5.0", diff --git a/scripts/generate-flow-vectors.ts b/scripts/generate-flow-vectors.ts index 93f122d9..d7c19c65 100644 --- a/scripts/generate-flow-vectors.ts +++ b/scripts/generate-flow-vectors.ts @@ -13,7 +13,7 @@ // serialization — the C++ side mirrors both (Kind::Undefined members are // skipped by stringify). -import { fileURLToPath } from 'node:url'; +import { fileURLToPath, URL } from 'node:url'; import { buildCheckRequestBody, decideDownload, diff --git a/scripts/prepublish.ts b/scripts/prepublish.ts deleted file mode 100644 index a925d185..00000000 --- a/scripts/prepublish.ts +++ /dev/null @@ -1,189 +0,0 @@ -#!/usr/bin/env bun - -import { access, readFile, writeFile } from 'node:fs/promises'; -import path from 'node:path'; -import { $ } from 'bun'; - -function normalizeVersion(version: string): string { - return version - .trim() - .replace(/^refs\/tags\//, '') - .replace(/^v/, ''); -} - -const SEMVER_REGEX = /^\d+\.\d+\.\d+(-.+)?$/; - -function isValidSemver(version: string): boolean { - return SEMVER_REGEX.test(version); -} - -function getVersionFromEnvironment(): string | null { - const candidates = [ - process.env.RELEASE_VERSION, - process.env.CI_COMMIT_TAG, - process.env.GITHUB_REF_TYPE === 'tag' ? process.env.GITHUB_REF_NAME : null, - process.env.GITHUB_REF?.startsWith('refs/tags/') - ? process.env.GITHUB_REF - : null, - ]; - - for (const candidate of candidates) { - if (candidate?.trim()) { - return normalizeVersion(candidate); - } - } - - return null; -} - -function getShellErrorMessage(error: unknown): string { - if ( - typeof error === 'object' && - error !== null && - 'stderr' in error && - typeof error.stderr === 'string' && - error.stderr.trim() - ) { - return error.stderr.trim(); - } - - if (error instanceof Error && error.message.trim()) { - return error.message.trim(); - } - - return 'Unknown git error'; -} - -async function getVersionFromGit(): Promise { - try { - return normalizeVersion(await $`git describe --tags --always`.text()); - } catch (error) { - const message = getShellErrorMessage(error); - - if (message.includes('detected dubious ownership')) { - throw new Error( - 'Git refused to read repository metadata because this checkout is not marked as safe. Configure safe.directory in CI or provide RELEASE_VERSION/GITHUB_REF_NAME.', - { cause: error } - ); - } - - throw new Error(`Unable to resolve publish version from git: ${message}`, { - cause: error, - }); - } -} - -async function resolveVersion(): Promise { - return getVersionFromEnvironment() ?? (await getVersionFromGit()); -} - -async function modifyPackageJson({ - version, -}: { - version: string; -}): Promise { - const packageJsonPath = path.join(__dirname, '..', 'package.json'); - - try { - await access(packageJsonPath); - } catch { - throw new Error(`package.json not found at ${packageJsonPath}`); - } - - console.log('Reading package.json...'); - const packageJsonContent = await readFile(packageJsonPath, 'utf-8'); - const packageJson = JSON.parse(packageJsonContent); - - packageJson.version = version; - - console.log('Writing modified package.json...'); - - await writeFile( - packageJsonPath, - JSON.stringify(packageJson, null, 2), - 'utf-8' - ); - - console.log('package.json has been modified for publishing'); -} - -function isGitHubCI(): boolean { - return process.env.GITHUB_ACTIONS === 'true'; -} - -function shouldSkipNativeBuild(): boolean { - return ( - process.argv.includes('--skip') || process.env.SKIP_NATIVE_BUILD === '1' - ); -} - -async function buildNativeArtifacts(): Promise { - console.log('Building Harmony HAR...'); - const harResult = Bun.spawnSync(['npm', 'run', 'build:harmony-har'], { - cwd: path.join(__dirname, '..'), - stdio: ['inherit', 'inherit', 'inherit'], - }); - if (harResult.exitCode !== 0) { - throw new Error( - `Harmony HAR build failed with exit code ${harResult.exitCode}` - ); - } - - console.log('Building Android SO...'); - const soResult = Bun.spawnSync(['npm', 'run', 'build:so'], { - cwd: path.join(__dirname, '..'), - stdio: ['inherit', 'inherit', 'inherit'], - }); - if (soResult.exitCode !== 0) { - throw new Error( - `Android SO build failed with exit code ${soResult.exitCode}` - ); - } -} - -async function main(): Promise { - try { - if (isGitHubCI()) { - const version = await resolveVersion(); - if (!isValidSemver(version)) { - throw new Error( - `Resolved version "${version}" is not a valid semantic version. ` + - `Please ensure RELEASE_VERSION, GITHUB_REF, or GITHUB_REF_NAME is set correctly, ` + - `or that the git history is fully fetched.` - ); - } - console.log(`Using publish version ${version}`); - await modifyPackageJson({ version }); - // Last gate for any CI publish path: publish.yml is expected to have - // replaced android/lib with CI-built binaries by now — verify here so a - // future workflow that skips that step can never silently ship stale - // committed .so files. Pure Node, runs in the HarmonyOS docker image too. - console.log('Verifying Android native libraries...'); - const verifyResult = Bun.spawnSync( - ['node', path.join(__dirname, 'verify-android-so.js')], - { stdio: ['inherit', 'inherit', 'inherit'] } - ); - if (verifyResult.exitCode !== 0) { - throw new Error( - `Android .so verification failed with exit code ${verifyResult.exitCode}` - ); - } - } else { - console.log( - 'ℹ️ Not in GitHub CI, skipping version resolution and package.json modification' - ); - if (shouldSkipNativeBuild()) { - console.log('ℹ️ --skip flag detected, skipping native artifacts build'); - } else { - await buildNativeArtifacts(); - } - } - - console.log('✅ Prepublish script completed successfully'); - } catch (error) { - console.error('❌ Prepublish script failed:', error); - process.exit(1); - } -} - -main(); diff --git a/scripts/verify-android-so.sh b/scripts/verify-android-so.sh deleted file mode 100755 index 82d53679..00000000 --- a/scripts/verify-android-so.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash -# Thin wrapper kept for backward compatibility; the actual verification lives -# in verify-android-so.js (pure-node ELF .dynsym reader, no binutils needed — -# llvm-nm/nm are absent on some CI images, e.g. the HarmonyOS docker image). -exec node "$(dirname "${BASH_SOURCE[0]}")/verify-android-so.js" From 8ac7bb59d664b6107c45a0292027811d4dffb45c Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:24:22 +0800 Subject: [PATCH 09/21] fix(ios): canonical config equality, lock-free digest check, bounded host waits - Config changes: persistConfiguration and hasJsRound compare configs by their canonical form (defaults filled, key order ignored, trailing '/' stripped and endpoints/queryUrls deduplicated). An equivalent config is a no-op; a real change bumps its own pushyNativeConfigGeneration, which the round commit checks next to pushyResetGeneration (now reset-only). - syncNativeConfig rejects with NATIVE_CONFIG_CONFLICT once the host called +configure:completion: in this process. - One PushyLoadRunnableConfig gate for crash-hold install, round start, prepareBundle and the round itself (same disabled/appKey rules). - switchVersion hashes the bundle outside the state lock (+bundleURL takes it on the main thread); under the lock it re-checks the record and the bundle's inode/size/mtime before switching. - prepareBundle runs the round on its own queue and waits a bounded time: new +prepareBundleWithTimeout:completion:, the old selector waits at most 30 minutes. Expiry reports failed/timeout. - tvOS purged-version restore: budget counts from process start, skipped when offline (nw_path_monitor) or with < 2s left, and never hashes the embedded bundle on that path. - Encoded constants decode lazily into a byte buffer; the URL regex is compiled once; one long-lived NSURLSession; OS version from NSProcessInfo instead of main-thread-only UIDevice; the endpoint loop is shared. - podspec: tvOS deployment target from react-native-tvos (or Expo), 15.1 fallback, instead of the iOS one; link Network on tvOS. - Purge-restore harness: late launch, offline and spent-budget cases plus canonical config equality. Co-Authored-By: Claude Opus 5.5 --- ios/RCTPushy/RCTPushy.h | 12 + ios/RCTPushy/RCTPushy.mm | 846 +++++++++++++----- ios/RCTPushy/RCTPushyConfiguration.mm | 25 +- react-native-update.podspec | 21 +- scripts/tests/ios-purge-restore/README.md | 15 +- scripts/tests/ios-purge-restore/extract.py | 13 +- .../ios-purge-restore/purge_restore_test.mm | 75 +- 7 files changed, 771 insertions(+), 236 deletions(-) diff --git a/ios/RCTPushy/RCTPushy.h b/ios/RCTPushy/RCTPushy.h index 21f225ca..add73525 100644 --- a/ios/RCTPushy/RCTPushy.h +++ b/ios/RCTPushy/RCTPushy.h @@ -26,8 +26,20 @@ typedef void (^RCTPushyBundlePreparationCompletion)(NSDictionary * Result keys: status (skipped/noUpdate/downloaded/failed/cancelled), reason, * hash (empty when nothing was installed), activated (selected for NEXT * launch, not a reload of the running instance). + * + * Waits at most 30 minutes for the round (longer than any round takes). */ + (void)prepareBundleWithCompletion:(RCTPushyBundlePreparationCompletion _Nullable)completion NS_SWIFT_NAME(prepareBundle(completion:)); +/** + * Same as prepareBundleWithCompletion:, but the completion runs after at most + * `timeout` seconds. On expiry the result is status failed, reason timeout; + * the round keeps running in the background and its result still applies to + * the next launch. + */ ++ (void)prepareBundleWithTimeout:(NSTimeInterval)timeout + completion:(RCTPushyBundlePreparationCompletion _Nullable)completion + NS_SWIFT_NAME(prepareBundle(timeout:completion:)); + @end diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index 8a7c78b4..daa00382 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -42,6 +42,11 @@ #include #include +#if TARGET_OS_TV +#import +#include +#include +#endif // Immutable host-facing snapshot; activated always means NEXT launch. static NSDictionary *PushyHostResult(NSString *status, NSString *reason, @@ -70,7 +75,21 @@ static NSString *const keyHashInfo = @"REACTNATIVECN_PUSHY_HASH_"; static NSString *const keyFirstLoadMarked = @"REACTNATIVECN_PUSHY_FIRSTLOADMARKED_KEY"; static NSString *const keyRolledBackMarked = @"REACTNATIVECN_PUSHY_ROLLEDBACKMARKED_KEY"; -static NSString *const KeyPackageUpdatedMarked = RCTPushyRevealText("0832d5f29aa549710b0939daf88ca058790f3dded7e888b451640d2ec3f69099bb43711523cafa8ea7bb4372132a"); +// Encoded constants (RCTPushyRevealText) are decoded on first use, not by +// static initializers at image load. +static NSString *PushyRevealOnce(NSString *__strong *slot, dispatch_once_t *once, + const char *hex) { + dispatch_once(once, ^{ + *slot = RCTPushyRevealText(hex); + }); + return *slot; +} + +static NSString *KeyPackageUpdatedMarked(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "0832d5f29aa549710b0939daf88ca058790f3dded7e888b451640d2ec3f69099bb43711523cafa8ea7bb4372132a"); +} // bundleHash cache: "|" where cacheKey identifies the // installed binary (packageVersion + embedded bundle size + mtime). Recomputed // only when the key changes, i.e. once per install. @@ -90,15 +109,43 @@ // file def static NSString * const BUNDLE_FILE_NAME = @"index.bundlejs"; static NSString * const SOURCE_PATCH_NAME = @"__diff.json"; -static NSString * const BUNDLE_PATCH_NAME = RCTPushyRevealText("3319f0d4b6c56a502c3b10fcdca0de7d4b3307e9"); +static NSString *BUNDLE_PATCH_NAME(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "3319f0d4b6c56a502c3b10fcdca0de7d4b3307e9"); +} // Protocol strings and version-info flags read by JS, stored encoded // (RCTPushyRevealText) so they do not appear as plain binary strings. -static NSString * const PushyOptionUrlKey = RCTPushyRevealText("2f07f0d0ba8e5d572e"); -static NSString * const PushyIpaDeltaSuffix = RCTPushyRevealText("741ee4d0e09b69512137"); -static NSString * const PushyPpkDeltaSuffix = RCTPushyRevealText("7407e4dae09b69512137"); -static NSString * const PushyInfoCrashHold = RCTPushyRevealText("3905f5c2a6b96d56212a19"); -static NSString * const PushyInfoForceBoot = RCTPushyRevealText("3c18e6d2aba9674a360d19ead5a695"); -static NSString * const PushyStatusNone = RCTPushyRevealText("3418c1c1aa8a7c40"); +static NSString *PushyOptionUrlKey(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "2f07f0d0ba8e5d572e"); +} +static NSString *PushyIpaDeltaSuffix(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "741ee4d0e09b69512137"); +} +static NSString *PushyPpkDeltaSuffix(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "7407e4dae09b69512137"); +} +static NSString *PushyInfoCrashHold(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "3905f5c2a6b96d56212a19"); +} +static NSString *PushyInfoForceBoot(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "3c18e6d2aba9674a360d19ead5a695"); +} +static NSString *PushyStatusNone(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "3418c1c1aa8a7c40"); +} #define VERSION_COMPLETE_FILE_NAME_LITERAL ".pushy-complete" static NSString * const VERSION_COMPLETE_FILE_NAME = @VERSION_COMPLETE_FILE_NAME_LITERAL; @@ -155,6 +202,14 @@ static NSTimeInterval PushyMonotonicNow(void) { return [object isKindOfClass:[NSDictionary class]] ? (NSDictionary *)object : nil; } +// Whether a non-empty record names this version under the current schema. +static BOOL PushyInstallRecordMatches(NSDictionary *record, NSString *hash) { + return [record[@"schema"] isKindOfClass:[NSNumber class]] + && [record[@"schema"] intValue] == pushy::install_record::kSchema + && [record[@"versionHash"] isKindOfClass:[NSString class]] + && [record[@"versionHash"] isEqualToString:hash]; +} + // Presence check (launch / dedup paths, no digest work): bundle present and // the record exists and, unless legacy-empty, names this version. static BOOL PushyHasCompletedVersionAtPath(NSString *versionDir, NSString *hash) { @@ -166,37 +221,69 @@ static BOOL PushyHasCompletedVersionAtPath(NSString *versionDir, NSString *hash) if (record == nil) { return NO; } - if (record.count == 0) { - return YES; + return record.count == 0 || PushyInstallRecordMatches(record, hash); +} + +// Identity of a file's bytes on disk without reading them: a rewrite or an +// atomic replacement (new inode) changes it. +struct PushyFileStamp { + bool exists = false; + dev_t device = 0; + ino_t inode = 0; + off_t size = 0; + struct timespec modified = {0, 0}; + + bool operator==(const PushyFileStamp &other) const { + return exists == other.exists && device == other.device && inode == other.inode + && size == other.size && modified.tv_sec == other.modified.tv_sec + && modified.tv_nsec == other.modified.tv_nsec; } - return [record[@"schema"] isKindOfClass:[NSNumber class]] - && [record[@"schema"] intValue] == pushy::install_record::kSchema - && [record[@"versionHash"] isKindOfClass:[NSString class]] - && [record[@"versionHash"] isEqualToString:hash]; + bool operator!=(const PushyFileStamp &other) const { return !(*this == other); } +}; + +static PushyFileStamp PushyStampFile(NSString *path) { + PushyFileStamp stamp; + struct stat info; + if (path != nil && stat(path.fileSystemRepresentation, &info) == 0) { + stamp.exists = true; + stamp.device = info.st_dev; + stamp.inode = info.st_ino; + stamp.size = info.st_size; + stamp.modified = info.st_mtimespec; + } + return stamp; } // Activation check: re-hashes the bundle when the record carries a digest. // Returns nil when the directory may be activated, the reason otherwise. -static NSString *PushyVerifyInstallForActivation(NSString *versionDir, NSString *hash) { +// Slow (a full read of the bundle): never call it under the state lock. On +// success *recordOut and *stampOut describe exactly what was verified, so the +// caller can confirm under the lock that nothing changed since. +static NSString *PushyVerifyInstallForActivation(NSString *versionDir, NSString *hash, + NSDictionary **recordOut, + PushyFileStamp *stampOut) { + NSString *bundlePath = [versionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]; + PushyFileStamp before = PushyStampFile(bundlePath); NSDictionary *record = PushyReadInstallRecord(versionDir); if (record == nil) { return [NSString stringWithFormat:@"Bundle version %@ has no valid completion record.", hash]; } + *recordOut = record; + *stampOut = before; if (record.count == 0) { return nil; } - if (![record[@"schema"] isKindOfClass:[NSNumber class]] - || [record[@"schema"] intValue] != pushy::install_record::kSchema - || ![record[@"versionHash"] isKindOfClass:[NSString class]] - || ![record[@"versionHash"] isEqualToString:hash]) { + if (!PushyInstallRecordMatches(record, hash)) { return [NSString stringWithFormat:@"Bundle version %@ completion record mismatch.", hash]; } NSString *expected = record[@"bundleSha256"]; if (![expected isKindOfClass:[NSString class]] || expected.length == 0) { return nil; } - std::string actual = pushy::digest::Sha256File( - PushyToStdString([versionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME])); + std::string actual = pushy::digest::Sha256File(PushyToStdString(bundlePath)); + if (PushyStampFile(bundlePath) != before) { + return [NSString stringWithFormat:@"Bundle version %@ changed while being verified.", hash]; + } if ([[expected lowercaseString] isEqualToString:[NSString stringWithUTF8String:actual.c_str()]]) { return nil; } @@ -326,11 +413,17 @@ typedef NS_ENUM(NSInteger, PushyType) { }; static std::atomic ignoreRollback{false}; +// Set once the host calls +configure:completion: in this process; JS +// syncNativeConfig then rejects with NATIVE_CONFIG_CONFLICT. Local literal +// until cpp/patch_core/error_codes.h carries the code (reconciled at merge). +static std::atomic pushyHostConfigured{false}; +static const char *const kPushyNativeConfigConflict = "NATIVE_CONFIG_CONFLICT"; // Whether the host asked +bundleURL for the bundle to load (the // isUsingBundleUrl constant, mirroring Android/Harmony). NO means the app // runs a bundle this SDK never resolved, so updates cannot take effect. static std::atomic pushyIsUsingBundleUrl{false}; -// Bumped by resetToPackagedBundle. The cold-start check runs for minutes and +// Bumped by resetToPackagedBundle only (config replacement has its own +// pushyNativeConfigGeneration). The cold-start check runs for minutes and // may already hold a decision when the app resets to the packaged bundle; it // samples this counter and abandons activation (and its response cache) when // the value moved, so an in-flight rescue can never resurrect the version the @@ -753,6 +846,7 @@ @interface RCTPushyOrchestrator : NSObject + (void)persistConfiguration:(NSString *)config; + (BOOL)hasRunnableConfig; + (NSDictionary *)prepareBundle; ++ (NSDictionary *)prepareBundleWithin:(NSTimeInterval)timeout; + (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion; #if TARGET_OS_TV && !DEBUG + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion @@ -760,9 +854,12 @@ + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion #endif + (void)recordJsRound:(NSString *)config; + (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime; ++ (BOOL)claimRound; ++ (void)runClaimedRoundWithDeadline:(NSTimeInterval)deadlineUptime; + (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime; + (void)runHoldRoundWithDeadline:(NSTimeInterval)deadlineUptime; + (BOOL)commitRoundWithGeneration:(uint64_t)generation + configGeneration:(uint64_t)configGeneration hashInfo:(NSDictionary *)hashInfoEntry activate:(NSString *)hashToActivate responseText:(NSString *)responseText @@ -801,6 +898,7 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation // itself would. Guarded by @synchronized (RCTPushyOrchestrator class). static NSString *pushyUnactivatedHash = nil; static uint64_t pushyUnactivatedGeneration = 0; +static uint64_t pushyUnactivatedConfigGeneration = 0; static NSUncaughtExceptionHandler *pushyPreviousExceptionHandler = NULL; // ≈ process start: scheduleFromColdStart runs during the first bundleURL. static NSTimeInterval pushyProcessAnchorUptime = 0; @@ -814,6 +912,8 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation static NSDictionary *pushyHostRoundResult = nil; static NSString *pushyHostRoundConfig = nil; static uint64_t pushyHostRoundGeneration = 0; +// Bumped by persistConfiguration when the config really changes (C1 +// equivalence fails); resets keep pushyResetGeneration to themselves. static std::atomic pushyNativeConfigGeneration{0}; static uint64_t pushyHostRoundConfigGeneration = 0; @@ -822,8 +922,57 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation // A held main thread freezes UI teardown; stay well under the watchdog. static const NSTimeInterval kPushyHoldBudgetMainThread = 3.5; // The purged-version restore blocks +bundleURL, usually inside -// application:didFinishLaunching: — stay clear of the launch watchdog. +// application:didFinishLaunching: — stay clear of the launch watchdog. The +// budget is measured from process start; with less than the minimum left the +// restore is not attempted. +#if TARGET_OS_TV && !DEBUG static const NSTimeInterval kPushyPurgeRestoreBudget = 12; +static const NSTimeInterval kPushyPurgeRestoreMinimumBudget = 2; + +// Seconds since the kernel started this process; 0 when unknown. +static NSTimeInterval PushyProcessElapsed(void) { + struct kinfo_proc info; + size_t size = sizeof(info); + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; + if (sysctl(mib, 4, &info, &size, NULL, 0) != 0 || size == 0) { + return 0; + } + struct timeval now; + gettimeofday(&now, NULL); + const struct timeval start = info.kp_proc.p_starttime; + NSTimeInterval elapsed = (now.tv_sec - start.tv_sec) + + (now.tv_usec - start.tv_usec) / 1e6; + return elapsed > 0 ? elapsed : 0; +} + +// NO only when the system positively reports no usable network path; an +// unknown state (no answer within the short wait) lets the restore try. +static BOOL PushyHasNetworkPath(void) { + dispatch_queue_t queue = dispatch_queue_create("cn.reactnative.pushy.path", DISPATCH_QUEUE_SERIAL); + nw_path_monitor_t monitor = nw_path_monitor_create(); + dispatch_semaphore_t answered = dispatch_semaphore_create(0); + __block nw_path_status_t status = nw_path_status_invalid; + nw_path_monitor_set_queue(monitor, queue); + nw_path_monitor_set_update_handler(monitor, ^(nw_path_t path) { + BOOL first = status == nw_path_status_invalid; + status = nw_path_get_status(path); + if (first) { + dispatch_semaphore_signal(answered); + } + }); + nw_path_monitor_start(monitor); + dispatch_semaphore_wait(answered, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 2)); + nw_path_monitor_cancel(monitor); + __block nw_path_status_t result; + dispatch_sync(queue, ^{ + result = status; + }); + return result != nw_path_status_unsatisfied; +} +#endif +// prepareBundleWithCompletion: (no explicit timeout) waits this long at most: +// past the longest round (8 query requests, then two 600s download phases). +static const NSTimeInterval kPushyHostPrepareDefaultTimeout = 1800; static void PushyMaybeHoldProcess(void) { // Once per process; a second crashing thread passes straight through @@ -964,6 +1113,32 @@ static void PushyInstallCrashHoldHandler(void) { } } +// Host preparation runs off the caller's thread, one at a time; the +// completion is delivered on the main queue. +static void PushyRunHostPreparation(NSDictionary *(^work)(void), + RCTPushyBundlePreparationCompletion completion) { + static dispatch_queue_t hostQueue; + static dispatch_once_t once; + dispatch_once(&once, ^{ + hostQueue = dispatch_queue_create("cn.reactnative.pushy.host-sync", DISPATCH_QUEUE_SERIAL); + }); + dispatch_async(hostQueue, ^{ + NSDictionary *result; + @try { + result = work(); + } @catch (NSException *exception) { + RCTLogWarn(@"RCTPushy -- native host sync failed: %@", exception.reason); + result = PushyHostResult(@"failed", @"internal_error", nil, NO); + } + if (completion != nil) { + NSDictionary *snapshot = [result copy]; + dispatch_async(dispatch_get_main_queue(), ^{ + completion(snapshot); + }); + } + }); +} + @implementation RCTPushy { dispatch_queue_t _fileQueue; bool hasListeners; @@ -1024,7 +1199,7 @@ + (NSURL *)resolveLaunchBundleURL:(NSString **)launchRolledBackVersionOut PushyToStdString(curBuildTime) ); if (sync.changed) { - [defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked]; + [defaults setObject:@(YES) forKey:KeyPackageUpdatedMarked()]; state = sync.state; PushyApplyStateToDefaults(defaults, state); } @@ -1120,6 +1295,10 @@ + (NSString *) rollback { + (void)configure:(NSDictionary *)options completion:(RCTPushyConfigurationCompletion)completion { + // From here on the host owns the config; syncNativeConfig refuses to + // overwrite it (set even when these options fail validation: the host's + // intent is what conflicts with a JS-owned config). + pushyHostConfigured.store(true); NSError *validationError = nil; // Snapshot nested mutable caller values before crossing a queue boundary. NSString *config = RCTPushyNormalizeConfiguration(options, &validationError); @@ -1148,26 +1327,17 @@ + (void)configure:(NSDictionary *)options + (void)prepareBundleWithCompletion:(RCTPushyBundlePreparationCompletion)completion { - static dispatch_queue_t hostQueue; - static dispatch_once_t once; - dispatch_once(&once, ^{ - hostQueue = dispatch_queue_create("cn.reactnative.pushy.host-sync", DISPATCH_QUEUE_SERIAL); - }); - dispatch_async(hostQueue, ^{ - NSDictionary *result; - @try { - result = [RCTPushyOrchestrator prepareBundle]; - } @catch (NSException *exception) { - RCTLogWarn(@"RCTPushy -- native host sync failed: %@", exception.reason); - result = PushyHostResult(@"failed", @"internal_error", nil, NO); - } - if (completion != nil) { - NSDictionary *snapshot = [result copy]; - dispatch_async(dispatch_get_main_queue(), ^{ - completion(snapshot); - }); - } - }); + PushyRunHostPreparation(^NSDictionary * { + return [RCTPushyOrchestrator prepareBundle]; + }, completion); +} + ++ (void)prepareBundleWithTimeout:(NSTimeInterval)timeout + completion:(RCTPushyBundlePreparationCompletion)completion +{ + PushyRunHostPreparation(^NSDictionary * { + return [RCTPushyOrchestrator prepareBundleWithin:timeout]; + }, completion); } + (BOOL)requiresMainQueueSetup @@ -1217,8 +1387,8 @@ - (NSDictionary *)constantsToExport [self clearInvalidFiles]; } - if ([[defaults objectForKey:KeyPackageUpdatedMarked] boolValue]) { - [defaults removeObjectForKey:KeyPackageUpdatedMarked]; + if ([[defaults objectForKey:KeyPackageUpdatedMarked()] boolValue]) { + [defaults removeObjectForKey:KeyPackageUpdatedMarked()]; [self clearInvalidFiles]; } }); @@ -1264,6 +1434,15 @@ - (instancetype)init // Provisioning for the native cold-start check (NATIVE_CHECKUPDATE_DESIGN // §10.1). Validate at write time: a corrupt config would otherwise // silently disable the native check forever with no signal. + if (pushyHostConfigured.load()) { + // Two owners of one config would overwrite each other on every + // launch; fail loudly instead of picking one. + PushyRejectError(reject, PushyErrorWithCode(kPushyNativeConfigConflict, + @"[NATIVE_CONFIG_CONFLICT] " + @"The host app called RCTPushy configure natively in this process; " + @"set nativeConfigSource: 'native' in the JS client options.")); + return; + } if (PushyStringIsBlank(config)) { PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS)); return; @@ -1494,7 +1673,7 @@ - (instancetype)init } } [defaults removeObjectForKey:keyFirstLoadMarked]; - [defaults removeObjectForKey:KeyPackageUpdatedMarked]; + [defaults removeObjectForKey:KeyPackageUpdatedMarked()]; // A cached response still advertises the version this reset just // removed; dropping it stops the JS side from reusing that answer. [defaults removeObjectForKey:keyNativeCheckCache]; @@ -1600,7 +1779,7 @@ - (NSError *)restartBridgeWithReason:(NSString *)reason - (void)performFetch:(PushyType)type options:(NSDictionary *)options callback:(void (^)(NSError *error))callback { - NSString *updateUrl = PushyOptionString(options, PushyOptionUrlKey); + NSString *updateUrl = PushyOptionString(options, PushyOptionUrlKey()); NSString *hash = PushyOptionString(options, @"hash"); if (PushyStringIsBlank(updateUrl) || !PushyIsSafePathComponent(hash)) { @@ -1827,7 +2006,7 @@ - (void)applyDeltaForHash:(NSString *)hash NSString *unzipDir = PushyStagingDirForVersionDir( [[RCTPushy downloadDir] stringByAppendingPathComponent:hash]); NSString *sourcePatch = [unzipDir stringByAppendingPathComponent:SOURCE_PATCH_NAME]; - NSString *bundlePatch = [unzipDir stringByAppendingPathComponent:BUNDLE_PATCH_NAME]; + NSString *bundlePatch = [unzipDir stringByAppendingPathComponent:BUNDLE_PATCH_NAME()]; NSString *destination = [unzipDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]; long long manifestBytes = RCTPushyFileSize(sourcePatch); @@ -1868,7 +2047,7 @@ - (void)applyDeltaForHash:(NSString *)hash entryNames.push_back(PushyToStdString(SOURCE_PATCH_NAME)); } if ([[NSFileManager defaultManager] fileExistsAtPath:bundlePatch isDirectory:NULL]) { - entryNames.push_back(PushyToStdString(BUNDLE_PATCH_NAME)); + entryNames.push_back(PushyToStdString(BUNDLE_PATCH_NAME())); } pushy::archive_patch::ArchivePatchPlan plan; @@ -1904,7 +2083,7 @@ - (void)applyDeltaForHash:(NSString *)hash // 存在时透传给 patch 内核执行 T(origin) → hpatch → T⁻¹;缺失走现状路径。 NSDictionary *hbcTransform = json[@"hbcTransform"]; if ([hbcTransform isKindOfClass:[NSDictionary class]]) { - NSDictionary *meta = hbcTransform[BUNDLE_PATCH_NAME]; + NSDictionary *meta = hbcTransform[BUNDLE_PATCH_NAME()]; if ([meta isKindOfClass:[NSDictionary class]]) { NSError *metaError = nil; NSData *metaData = [NSJSONSerialization dataWithJSONObject:meta options:0 error:&metaError]; @@ -1933,40 +2112,80 @@ - (BOOL)switchVersion:(NSString *)hash error:(NSError **)error return NO; } - __block NSError *switchError = nil; - PushyWithStateLock(^{ - // Same rule as Android: only a version this SDK recorded as completely - // installed (bundle + marker) may be activated. Versions activated - // before markers existed are grandfathered through current/last - // state; any other markerless directory may be a crash-left partial - // install or a directory something else put there. - NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash]; + // Same rule as Android: only a version this SDK recorded as completely + // installed (bundle + marker) may be activated. Versions activated before + // markers existed are grandfathered through current/last state; any other + // markerless directory may be a crash-left partial install or a directory + // something else put there. + NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash]; + std::string hashStd = PushyToStdString(hash); + BOOL (^isLegacyActivated)(void) = ^BOOL { pushy::state::State state = PushyStateFromDefaults(PushyDefaults()); - std::string hashStd = PushyToStdString(hash); - BOOL legacyActivated = state.current_version == hashStd || state.last_version == hashStd; - if (!PushyHasCompletedVersionAtPath(versionDir, hash) && !legacyActivated) { - BOOL hasBundle = [[NSFileManager defaultManager] fileExistsAtPath: - [versionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]]; - switchError = PushyErrorWithCode( - pushy::error_codes::kSwitchVersionFailed, - [NSString stringWithFormat:@"Bundle version %@ %@", hash, - hasBundle ? @"is incomplete." : @"not found."]); - return; - } - if (!legacyActivated) { + return state.current_version == hashStd || state.last_version == hashStd; + }; + NSError *(^incompleteError)(void) = ^NSError * { + BOOL hasBundle = [[NSFileManager defaultManager] fileExistsAtPath: + [versionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]]; + return PushyErrorWithCode( + pushy::error_codes::kSwitchVersionFailed, + [NSString stringWithFormat:@"Bundle version %@ %@", hash, + hasBundle ? @"is incomplete." : @"not found."]); + }; + + // The digest check reads the whole bundle, so it runs OUTSIDE the state + // lock: +bundleURL takes that lock on the main thread at launch. + __block BOOL legacyActivated = NO; + PushyWithStateLock(^{ + legacyActivated = isLegacyActivated(); + }); + NSError *switchError = nil; + NSDictionary *verifiedRecord = nil; + PushyFileStamp verifiedStamp; + BOOL verified = NO; + if (!legacyActivated) { + if (!PushyHasCompletedVersionAtPath(versionDir, hash)) { + switchError = incompleteError(); + } else { // The record's bundle digest must match the bytes on disk before // the next launch is pointed at them. - NSString *reason = PushyVerifyInstallForActivation(versionDir, hash); + NSString *reason = PushyVerifyInstallForActivation( + versionDir, hash, &verifiedRecord, &verifiedStamp); if (reason != nil) { switchError = PushyErrorWithCode(pushy::error_codes::kSwitchVersionFailed, reason); - return; + } else { + verified = YES; } } - PushySwitchVersionLocked(hash); - }); - if (switchError != nil) { + } + + __block NSError *lockedError = switchError; + if (lockedError == nil) { + NSDictionary *record = verifiedRecord; + PushyFileStamp stamp = verifiedStamp; + PushyWithStateLock(^{ + if (!isLegacyActivated()) { + // Re-check cheaply that the directory is still the one that + // was verified: same record, same bundle file (size, mtime, + // inode). + if (!PushyHasCompletedVersionAtPath(versionDir, hash)) { + lockedError = incompleteError(); + return; + } + NSString *bundlePath = [versionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]; + if (!verified || ![PushyReadInstallRecord(versionDir) isEqual:record] + || PushyStampFile(bundlePath) != stamp) { + lockedError = PushyErrorWithCode( + pushy::error_codes::kSwitchVersionFailed, + [NSString stringWithFormat:@"Bundle version %@ changed while switching.", hash]); + return; + } + } + PushySwitchVersionLocked(hash); + }); + } + if (lockedError != nil) { if (error != NULL) { - *error = switchError; + *error = lockedError; } return NO; } @@ -2128,9 +2347,9 @@ - (NSString *)zipExtension:(PushyType)type { switch (type) { case PushyTypePatchFromPackage: - return PushyIpaDeltaSuffix; + return PushyIpaDeltaSuffix(); case PushyTypePatchFromPpk: - return PushyPpkDeltaSuffix; + return PushyPpkDeltaSuffix(); case PushyTypeFullDownload: break; } @@ -2245,6 +2464,33 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task @end +// One long-lived delegate session (not the shared one) so the redirect guard +// sees the 3xx before it is followed; completion handlers still receive the +// final response. Never invalidated: it lives as long as the process. +static NSURLSession *PushyRequestSession(void) { + static NSURLSession *session; + static dispatch_once_t once; + dispatch_once(&once, ^{ + session = [NSURLSession + sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:[PushyRequestRedirectGuard new] + delegateQueue:nil]; + }); + return session; +} + +// Same shape as -[UIDevice systemVersion] ("17.4", "17.4.1"), which is +// main-thread-only UIKit; the round runs on a background queue. +static NSString *PushyOsVersion(void) { + NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; + if (version.patchVersion > 0) { + return [NSString stringWithFormat:@"%ld.%ld.%ld", (long)version.majorVersion, + (long)version.minorVersion, (long)version.patchVersion]; + } + return [NSString stringWithFormat:@"%ld.%ld", (long)version.majorVersion, + (long)version.minorVersion]; +} + // Blocking JSON HTTP round-trip on the orchestrator's utility thread. Returns // the response body on 2xx, nil on any failure. The semaphore timeout is a // backstop over the request's own timeoutInterval. @@ -2264,14 +2510,7 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task } dispatch_semaphore_t sem = dispatch_semaphore_create(0); __block NSString *result = nil; - // A delegate session (not the shared one) so the redirect guard sees the - // 3xx before it is followed; the completion handler still receives the - // final response. - NSURLSession *session = [NSURLSession - sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] - delegate:[PushyRequestRedirectGuard new] - delegateQueue:nil]; - NSURLSessionDataTask *task = [session dataTaskWithRequest:request + NSURLSessionDataTask *task = [PushyRequestSession() dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { NSHTTPURLResponse *httpResponse = [response isKindOfClass:[NSHTTPURLResponse class]] @@ -2285,9 +2524,6 @@ - (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task dispatch_semaphore_signal(sem); }]; [task resume]; - // The session retains its delegate until invalidated: let this one task - // settle, then release everything. - [session finishTasksAndInvalidate]; if (dispatch_semaphore_wait( sem, dispatch_time(DISPATCH_TIME_NOW, (int64_t)((timeout + 5) * NSEC_PER_SEC))) != 0) { @@ -2313,6 +2549,146 @@ static BOOL PushyIsValidResponse(NSString *responseText) { return flowcore::IsValidResponse(PushyToStdString(responseText)) ? YES : NO; } +// Canonical config equality, shared with the JS, Android and Harmony sides: +// two config JSON strings are equivalent iff their canonical forms deep-equal. +// Canonical form: the parsed object with absent keys defaulted +// (disabled:false, queryUrls:[], rnu:"", rn:"", afterDownload:"none") and the +// endpoints/queryUrls entries stripped of trailing '/' and deduplicated in +// first-occurrence order; key order is irrelevant. Unparseable is never +// equivalent. Lets a host-owned config (sorted keys) and the same config as JS +// serializes it count as one, instead of a "change" that cancels the round. +static BOOL PushyJsonIsBool(id value) { + return [value isKindOfClass:NSNumber.class] + && CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID(); +} + +static BOOL PushyJsonEqual(id a, id b) { + if (PushyJsonIsBool(a) != PushyJsonIsBool(b)) { + return NO; // false is not 0 + } + if ([a isKindOfClass:NSDictionary.class] && [b isKindOfClass:NSDictionary.class]) { + if ([a count] != [b count]) { + return NO; + } + for (id key in (NSDictionary *)a) { + id other = ((NSDictionary *)b)[key]; + if (other == nil || !PushyJsonEqual(((NSDictionary *)a)[key], other)) { + return NO; + } + } + return YES; + } + if ([a isKindOfClass:NSArray.class] && [b isKindOfClass:NSArray.class]) { + if ([a count] != [b count]) { + return NO; + } + for (NSUInteger i = 0; i < [a count]; i++) { + if (!PushyJsonEqual(((NSArray *)a)[i], ((NSArray *)b)[i])) { + return NO; + } + } + return YES; + } + return [a isEqual:b]; +} + +static id PushyCanonicalUrlList(id value) { + if (![value isKindOfClass:NSArray.class]) { + return value; + } + NSMutableArray *result = [NSMutableArray array]; + for (id item in (NSArray *)value) { + id entry = [item isKindOfClass:NSString.class] ? PushyNormalizeEndpointBase(item) : item; + BOOL seen = NO; + for (id existing in result) { + if (PushyJsonEqual(existing, entry)) { + seen = YES; + break; + } + } + if (!seen) { + [result addObject:entry]; + } + } + return result; +} + +static NSDictionary *PushyCanonicalConfig(NSString *json) { + NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; + id object = data == nil ? nil + : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (![object isKindOfClass:NSDictionary.class]) { + return nil; + } + NSMutableDictionary *config = [object mutableCopy]; + NSDictionary *defaults = @{@"disabled": @NO, @"queryUrls": @[], @"rnu": @"", + @"rn": @"", @"afterDownload": @"none"}; + for (NSString *key in defaults) { + if (config[key] == nil) { + config[key] = defaults[key]; + } + } + for (NSString *key in @[@"endpoints", @"queryUrls"]) { + if (config[key] != nil) { + config[key] = PushyCanonicalUrlList(config[key]); + } + } + return config; +} + +static BOOL PushyConfigsEquivalent(NSString *a, NSString *b) { + NSDictionary *left = a == nil ? nil : PushyCanonicalConfig(a); + NSDictionary *right = b == nil ? nil : PushyCanonicalConfig(b); + return left != nil && right != nil && PushyJsonEqual(left, right); +} + +// The persisted config a round may run with. Every gate (crash-hold handler +// install, round start, prepareBundle, the round itself) reads it through +// PushyLoadRunnableConfig, so they agree on what "configured" and "disabled" +// mean. disabled follows JS truthiness, like the flow core. +struct PushyRunnableConfig { + NSString *json; + flowjson::Value value; + NSString *appKey; +}; + +// YES when a round may run; otherwise *reason is not_configured, disabled or +// invalid_config. +static BOOL PushyLoadRunnableConfig(PushyRunnableConfig *config, NSString **reason) { + NSString *failure = nil; + NSString *json = [PushyDefaults() stringForKey:keyNativeConfig]; + config->json = [json copy]; + if (json.length == 0) { + failure = @"not_configured"; + } else { + bool ok = false; + flowjson::Value value = flowjson::Parse(PushyToStdString(json), &ok); + NSString *appKey = ok && value.IsObject() + ? PushyFromStdString(value.Get("appKey").AsString()) : nil; + if (!ok || !value.IsObject()) { + failure = @"invalid_config"; + } else if (value.Get("disabled").Truthy()) { + failure = @"disabled"; + } else if (appKey.length == 0) { + failure = @"invalid_config"; + } else { + config->value = value; + config->appKey = appKey; + } + } + if (reason != NULL) { + *reason = failure; + } + return failure == nil; +} + +// Host-facing result for a config that cannot run: absent or disabled is a +// skip, a malformed one a failure. +static NSDictionary *PushyUnrunnableConfigResult(NSString *reason) { + BOOL skip = [reason isEqualToString:@"not_configured"] || [reason isEqualToString:@"disabled"]; + return PushyHostResult(skip ? @"skipped" : @"failed", reason, nil, NO); +} + @implementation RCTPushyOrchestrator + (void)persistConfiguration:(NSString *)config { @@ -2321,12 +2697,16 @@ + (void)persistConfiguration:(NSString *)config { if ([defaults stringForKey:keyUuid].length == 0) { [defaults setObject:[NSUUID UUID].UUIDString forKey:keyUuid]; } - if ([[defaults stringForKey:keyNativeConfig] isEqualToString:config]) { + if (PushyConfigsEquivalent([defaults stringForKey:keyNativeConfig], config)) { + // The same config in another serialization (key order, trailing + // slashes, defaulted keys). Not a change: the stored text, the + // response cache and any in-flight round all stay valid. return; } - // The same generation protects reset and replacement of the request - // identity/policy, including a late crash-rescue activation. - pushyResetGeneration.fetch_add(1); + // A different request identity/policy: rounds started under the old + // one must not commit (pushyNativeConfigGeneration, checked by + // commitRoundWithGeneration next to the reset generation), and the + // cached response and JS-completed marker belong to the old one. pushyNativeConfigGeneration.fetch_add(1); [defaults setObject:config forKey:keyNativeConfig]; [defaults removeObjectForKey:keyNativeCheckCache]; @@ -2338,14 +2718,8 @@ + (void)persistConfiguration:(NSString *)config { } + (BOOL)hasRunnableConfig { - NSString *json = [PushyDefaults() stringForKey:keyNativeConfig]; - if (json.length == 0) { - return NO; - } - bool ok = false; - flowjson::Value config = flowjson::Parse(PushyToStdString(json), &ok); - return ok && config.IsObject() && !config.Get("disabled").Truthy() - && !config.Get("appKey").AsString().empty(); + PushyRunnableConfig config; + return PushyLoadRunnableConfig(&config, NULL); } // Process-wide round state, set up by whichever launch path needs a round @@ -2360,8 +2734,9 @@ + (void)prepareProcess:(NSString *)launchRolledBackVersion { pushyLaunchRolledBackForHold = [launchRolledBackVersion copy]; pushySyncReady.store(true); // The crash-hold rescue shares the orchestrator's rollout gate: no - // persisted config, no handler (§11.3). - if ([PushyDefaults() stringForKey:keyNativeConfig].length > 0) { + // runnable config, no handler (§11.3). persistConfiguration installs + // it once a runnable config arrives later in the process. + if ([self hasRunnableConfig]) { PushyInstallCrashHoldHandler(); } }); @@ -2395,7 +2770,8 @@ + (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion { #if TARGET_OS_TV && !DEBUG // Runs this process's round before the first bundle loads, blocking the -// caller for at most kPushyPurgeRestoreBudget. The state no longer has a +// caller for at most what is left of kPushyPurgeRestoreBudget since process +// start. The state no longer has a // current version, so the server answers with the latest version for the // packaged bundle, and the round activates it while the restore window is // open. Returns NO when no round can run; otherwise the caller must resolve @@ -2410,18 +2786,32 @@ + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion @"configured; launching the packaged bundle", purgedVersion); return NO; } + // Offline: the round could only burn the launch budget. + if (!PushyHasNetworkPath()) { + RCTLogWarn(@"RCTPushy -- version %@ was purged and there is no network; " + @"launching the packaged bundle", purgedVersion); + return NO; + } + // The launch watchdog counts from process start, not from this call: + // whatever the host did before resolving the bundle is already spent. + NSTimeInterval budget = kPushyPurgeRestoreBudget - PushyProcessElapsed(); + if (budget < kPushyPurgeRestoreMinimumBudget) { + RCTLogWarn(@"RCTPushy -- version %@ was purged and the launch budget is " + @"spent; launching the packaged bundle", purgedVersion); + return NO; + } PushyWithStateLock(^{ pushyPurgeRestoreWindowOpen = true; }); pushyPurgeRestoreActive.store(true); - NSTimeInterval deadline = PushyMonotonicNow() + kPushyPurgeRestoreBudget; + NSTimeInterval deadline = PushyMonotonicNow() + budget; dispatch_semaphore_t done = dispatch_semaphore_create(0); dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ [self startRoundWithDeadline:deadline]; dispatch_semaphore_signal(done); }); BOOL timedOut = dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(kPushyPurgeRestoreBudget * NSEC_PER_SEC))) != 0; + (int64_t)(budget * NSEC_PER_SEC))) != 0; // The round's deadline does not bound a slow response (the request // timeout is an idle timeout) nor a version already on disk, so the close // is what stops a late activation — not the deadline. @@ -2440,39 +2830,47 @@ + (BOOL)restorePurgedLaunch:(NSString *)purgedVersion #endif + (NSDictionary *)prepareBundle { + return [self prepareBundleWithin:kPushyHostPrepareDefaultTimeout]; +} + +// Waits at most `timeout` seconds for this process's round. The round itself +// is not cut short by the host's wait (its result still serves the next +// launch); the host gets failed/timeout instead of a result. ++ (NSDictionary *)prepareBundleWithin:(NSTimeInterval)timeout { #if DEBUG return PushyHostResult(@"skipped", @"debug", nil, NO); #else if (!pushySyncReady.load()) { return PushyHostResult(@"skipped", @"not_initialized", nil, NO); } - NSString *configJson = [PushyDefaults() stringForKey:keyNativeConfig]; - if (configJson.length == 0) { - return PushyHostResult(@"skipped", @"not_configured", nil, NO); - } - id config = [NSJSONSerialization JSONObjectWithData: - [configJson dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; - if (![config isKindOfClass:NSDictionary.class]) { - return PushyHostResult(@"failed", @"invalid_config", nil, NO); - } - id disabled = config[@"disabled"]; - if ([disabled respondsToSelector:@selector(boolValue)] && [disabled boolValue]) { - return PushyHostResult(@"skipped", @"disabled", nil, NO); - } - id appKey = config[@"appKey"]; - if (![appKey isKindOfClass:NSString.class] || [appKey length] == 0) { - return PushyHostResult(@"failed", @"invalid_config", nil, NO); + PushyRunnableConfig runnable; + NSString *reason = nil; + if (!PushyLoadRunnableConfig(&runnable, &reason)) { + return PushyUnrunnableConfigResult(reason); + } + NSString *configJson = runnable.json; + // Run the round on its own queue rather than this caller's thread, so the + // wait below can be bounded. + if ([self claimRound]) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + [self runClaimedRoundWithDeadline:0]; + }); } - [self startRoundWithDeadline:0]; // A group is broadcast-style. Sharing the rescue semaphore would let one // waiter consume the only signal and leave the other waiting forever. if (!pushyRoundStarted.load()) { return PushyHostResult(@"skipped", @"config_changed", nil, NO); } - dispatch_group_wait(pushyHostRoundGroup, DISPATCH_TIME_FOREVER); + if (!(timeout > 0)) { + timeout = 0; // also NaN + } + if (dispatch_group_wait(pushyHostRoundGroup, dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(MIN(timeout, (NSTimeInterval)(INT64_MAX / NSEC_PER_SEC)) * NSEC_PER_SEC))) != 0) { + return PushyHostResult(@"failed", @"timeout", nil, NO); + } if (pushyHostRoundConfigGeneration != pushyNativeConfigGeneration.load() - || ![configJson isEqualToString:pushyHostRoundConfig] - || ![configJson isEqualToString:[PushyDefaults() stringForKey:keyNativeConfig]]) { + || !PushyConfigsEquivalent(configJson, pushyHostRoundConfig) + || !PushyConfigsEquivalent(configJson, [PushyDefaults() stringForKey:keyNativeConfig])) { return PushyHostResult(@"cancelled", @"config_changed", nil, NO); } if (pushyHostRoundGeneration != pushyResetGeneration.load()) { @@ -2488,8 +2886,8 @@ + (void)recordJsRound:(NSString *)config { } } -// True when JS already obtained a valid response in this process for the -// exact config the native round would use (§10.3): the delayed round is +// True when JS already obtained a valid response in this process for a config +// equivalent to the one the native round would use (§10.3): the delayed round is // then a duplicate request. Only the scheduled round asks — the crash-rescue // path still runs, JS is dead by then. + (BOOL)hasJsRound { @@ -2500,22 +2898,30 @@ + (BOOL)hasJsRound { if (jsConfig == nil) { return NO; } - NSString *persisted = [PushyDefaults() stringForKey:keyNativeConfig]; - return persisted != nil && [persisted isEqualToString:jsConfig]; + return PushyConfigsEquivalent([PushyDefaults() stringForKey:keyNativeConfig], jsConfig); } // Runs the process's single round on the calling thread if nobody has // started it yet. deadlineUptime > 0 (crash rescue) caps every HTTP call and // download phase to the remaining budget. + (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime { - // Missing/disabled configuration is a preflight skip, not a used round. + if ([self claimRound]) { + [self runClaimedRoundWithDeadline:deadlineUptime]; + } +} + +// Claims this process's single round. Missing/disabled configuration is a +// preflight skip, not a used round. A YES must be followed by exactly one +// runClaimedRoundWithDeadline:. ++ (BOOL)claimRound { if (![self hasRunnableConfig]) { - return; + return NO; } bool expected = false; - if (!pushyRoundStarted.compare_exchange_strong(expected, true)) { - return; - } + return pushyRoundStarted.compare_exchange_strong(expected, true); +} + ++ (void)runClaimedRoundWithDeadline:(NSTimeInterval)deadlineUptime { @try { [self runOnce:pushyLaunchRolledBackForHold deadline:deadlineUptime]; } @catch (NSException *exception) { @@ -2553,9 +2959,11 @@ + (void)runHoldRoundWithDeadline:(NSTimeInterval)deadlineUptime { + (void)activatePendingVersion { NSString *hash = nil; uint64_t generation = 0; + uint64_t configGeneration = 0; @synchronized (self) { hash = pushyUnactivatedHash; generation = pushyUnactivatedGeneration; + configGeneration = pushyUnactivatedConfigGeneration; } if (hash == nil) { return; @@ -2576,10 +2984,11 @@ + (void)activatePendingVersion { [NSJSONSerialization JSONObjectWithData:existingData options:0 error:nil]; if ([parsed isKindOfClass:[NSDictionary class]]) { NSMutableDictionary *merged = [parsed mutableCopy]; - merged[PushyInfoCrashHold] = @YES; + merged[PushyInfoCrashHold()] = @YES; hashInfoEntry = @{@"hash": hash, @"info": merged}; } BOOL committed = [self commitRoundWithGeneration:generation + configGeneration:configGeneration hashInfo:hashInfoEntry activate:hash responseText:nil @@ -2593,7 +3002,7 @@ + (void)activatePendingVersion { } NSLog(@"RCTPushy -- crash hold: activated downloaded version %@", hash); } else { - NSLog(@"RCTPushy -- crash hold: reset since download, dropping activation"); + NSLog(@"RCTPushy -- crash hold: reset or new config since download, dropping activation"); } } @@ -2610,40 +3019,31 @@ + (RCTPushy *)engine { } + (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime { + // Generations first, config second: a config replaced in between is + // caught by the commit's generation check. const uint64_t resetGeneration = pushyResetGeneration.load(); + const uint64_t configGeneration = pushyNativeConfigGeneration.load(); pushyHostRoundGeneration = resetGeneration; - pushyHostRoundConfigGeneration = pushyNativeConfigGeneration.load(); + pushyHostRoundConfigGeneration = configGeneration; pushyHostRoundResult = PushyHostResult(@"failed", @"check_failed", nil, NO); NSUserDefaults *defaults = PushyDefaults(); - NSString *configJson = [defaults stringForKey:keyNativeConfig]; - pushyHostRoundConfig = [configJson copy]; - if (configJson.length == 0) { - pushyHostRoundResult = PushyHostResult(@"skipped", @"not_configured", nil, NO); - return; - } - bool ok = false; - flowjson::Value config = flowjson::Parse(PushyToStdString(configJson), &ok); - if (!ok || !config.IsObject()) { - pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_config", nil, NO); - return; - } - if (config.Get("disabled").Truthy()) { - pushyHostRoundResult = PushyHostResult(@"skipped", @"disabled", nil, NO); - return; - } - NSString *appKey = PushyFromStdString(config.Get("appKey").AsString()); - if (appKey.length == 0) { - pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_config", nil, NO); + PushyRunnableConfig config; + NSString *reason = nil; + BOOL runnable = PushyLoadRunnableConfig(&config, &reason); + pushyHostRoundConfig = config.json; + if (!runnable) { + pushyHostRoundResult = PushyUnrunnableConfigResult(reason); return; } // Preserve the interrupted-round breadcrumb and reset-safe atomic commit. [defaults setObject:@YES forKey:keyNativeCheckIncomplete]; @try { - [self runConfiguredRound:config - configJson:configJson - appKey:appKey + [self runConfiguredRound:config.value + configJson:config.json + appKey:config.appKey launchRolledBackVersion:launchRolledBackVersion resetGeneration:resetGeneration + configGeneration:configGeneration deadline:deadlineUptime]; } @finally { [defaults removeObjectForKey:keyNativeCheckIncomplete]; @@ -2655,6 +3055,7 @@ + (void)runConfiguredRound:(const flowjson::Value &)config appKey:(NSString *)appKey launchRolledBackVersion:(NSString *)launchRolledBackVersion resetGeneration:(uint64_t)resetGeneration + configGeneration:(uint64_t)configGeneration deadline:(NSTimeInterval)deadlineUptime { NSUserDefaults *defaults = PushyDefaults(); NSString *packageVersion = @@ -2688,7 +3089,7 @@ + (void)runConfiguredRound:(const flowjson::Value &)config cInfo.Set("rnu", config.Get("rnu")); cInfo.Set("rn", config.Get("rn")); cInfo.Set("os", flowjson::Value::String(PushyToStdString([NSString - stringWithFormat:@"%@ %@", PushyOsName, [[UIDevice currentDevice] systemVersion]]))); + stringWithFormat:@"%@ %@", PushyOsName, PushyOsVersion()]))); cInfo.Set("uuid", flowjson::Value::String(PushyToStdString(uuid))); flowjson::Value input = flowjson::Value::Object(); @@ -2701,11 +3102,12 @@ + (void)runConfiguredRound:(const flowjson::Value &)config input.Set("cInfo", cInfo); input.Set("supportedDiffVersion", flowjson::Value::Number(pushy::hbc::kSupportedDiffVersion)); - // Cache-only while a crash is being held (§11.3 budget); computed (and - // shared with JS getBundleHash) on the ordinary delayed round. + // Cache-only while a crash is being held (§11.3 budget) or the launch is + // blocked on the tvOS purged-version restore; computed (and shared with JS + // getBundleHash) on the ordinary delayed round. input.Set("bundleHash", - flowjson::Value::String(PushyToStdString( - PushyBundleHashSync(!pushyCrashHoldActive.load())))); + flowjson::Value::String(PushyToStdString(PushyBundleHashSync( + !(pushyCrashHoldActive.load() || pushyPurgeRestoreActive.load()))))); std::string bodyJson = flowjson::Stringify(flowcore::BuildRequestBody(input)); @@ -2731,6 +3133,7 @@ + (void)runConfiguredRound:(const flowjson::Value &)config config.Get("afterDownload").AsString()); if (decision.Get("action").AsString() != "download") { BOOL committed = [self commitRoundWithGeneration:resetGeneration + configGeneration:configGeneration hashInfo:nil activate:nil responseText:responseText @@ -2739,7 +3142,7 @@ + (void)runConfiguredRound:(const flowjson::Value &)config responseAt:responseAtSeconds activated:NULL]; pushyHostRoundResult = committed - ? PushyHostResult(PushyStatusNone, PushyFromStdString(decision.Get("reason").AsString()), nil, NO) + ? PushyHostResult(PushyStatusNone(), PushyFromStdString(decision.Get("reason").AsString()), nil, NO) : PushyHostResult(@"cancelled", @"reset", nil, NO); RCTLogInfo(@"RCTPushy -- native sync: nothing to do (%s)", decision.Get("reason").AsString().c_str()); @@ -2761,6 +3164,7 @@ + (void)runConfiguredRound:(const flowjson::Value &)config } if (!downloaded) { BOOL committed = [self commitRoundWithGeneration:resetGeneration + configGeneration:configGeneration hashInfo:nil activate:nil responseText:responseText @@ -2788,10 +3192,10 @@ + (void)runConfiguredRound:(const flowjson::Value &)config // survives to markSuccess. Only the server-sent directive counts — a // silent-strategy activation is ordinary delivery. if (info.Get("config").Get("forceBoot").Truthy()) { - versionInfo[PushyInfoForceBoot] = @YES; + versionInfo[PushyInfoForceBoot()] = @YES; } if (pushyCrashHoldActive.load()) { - versionInfo[PushyInfoCrashHold] = @YES; + versionInfo[PushyInfoCrashHold()] = @YES; } // Silent strategies or a server-marked forceBoot version (per-version // remote override — the brick rescue) activate for the next launch; @@ -2804,6 +3208,7 @@ + (void)runConfiguredRound:(const flowjson::Value &)config || pushyPurgeRestoreActive.load(); BOOL activated = NO; BOOL committed = [self commitRoundWithGeneration:resetGeneration + configGeneration:configGeneration hashInfo:@{@"hash": hash, @"info": versionInfo} activate:activate ? hash : nil responseText:responseText @@ -2825,6 +3230,7 @@ + (void)runConfiguredRound:(const flowjson::Value &)config @synchronized (self) { pushyUnactivatedHash = [hash copy]; pushyUnactivatedGeneration = resetGeneration; + pushyUnactivatedConfigGeneration = configGeneration; } RCTLogInfo(@"RCTPushy -- native sync: downloaded %@, activation left to JS", hash); } @@ -2835,10 +3241,11 @@ + (void)runConfiguredRound:(const flowjson::Value &)config // Everything a round persists — version info, the activation, the response // cache — is written inside ONE state-lock acquisition that first re-checks the -// reset generation. resetToPackagedBundle bumps that generation under the same +// reset and config generations. resetToPackagedBundle bumps that generation under the same // lock, so there is no compare-and-act window: either the whole round commits, // or the reset wins and none of it does. + (BOOL)commitRoundWithGeneration:(uint64_t)generation + configGeneration:(uint64_t)configGeneration hashInfo:(NSDictionary *)hashInfoEntry activate:(NSString *)hashToActivate responseText:(NSString *)responseText @@ -2864,6 +3271,11 @@ + (BOOL)commitRoundWithGeneration:(uint64_t)generation if (pushyResetGeneration.load() != generation) { return; } + // The config the round ran with was replaced (persistConfiguration + // bumps this under the same lock). + if (pushyNativeConfigGeneration.load() != configGeneration) { + return; + } NSString *activation = hashToActivate; NSDictionary *info = hashInfoEntry[@"info"]; // The tvOS purged-version restore round (a round commit, not the @@ -2924,43 +3336,59 @@ + (NSString *)runQueryRequest:(const flowjson::Value &)config appKey:(NSString *)appKey body:(NSString *)body deadline:(NSTimeInterval)deadlineUptime { - double sample = arc4random() / 4294967296.0; - flowjson::Value ordered = - flowcore::OrderEndpointCandidates(config.Get("endpoints"), sample); NSMutableSet *tried = [NSMutableSet set]; const NSUInteger maxHttpAttempts = 8; - NSUInteger httpAttempts = 0; - for (const auto &endpoint : ordered.elements()) { - NSString *base = PushyNormalizeEndpointBase( - PushyFromStdString(endpoint.AsString())); - if (base.length == 0 || [tried containsObject:base]) { - continue; - } + __block NSUInteger httpAttempts = 0; + // Spends one attempt of the shared budget. NO once the attempt cap or the + // deadline is exhausted (the round must stop issuing requests); the + // per-request timeout lands in *timeout otherwise. + BOOL (^takeAttempt)(NSTimeInterval *) = ^BOOL(NSTimeInterval *timeout) { if (httpAttempts++ >= maxHttpAttempts) { - return nil; - } - [tried addObject:base]; - NSTimeInterval timeout = PushyQueryRequestTimeout(deadlineUptime); - if (timeout <= 0) { - return nil; + return NO; } - NSString *response = PushyHttpRequest( - [NSString stringWithFormat:@"%@%@%@", base, RCTPushyQueryPath(), appKey], - @"POST", body, timeout); - if (PushyIsValidResponse(response)) { - return response; + *timeout = PushyQueryRequestTimeout(deadlineUptime); + return *timeout > 0; + }; + // Tries each not-yet-tried endpoint base in order and returns the first + // valid response; *stop is set when the budget ran out. + NSString *(^queryEndpoints)(const flowjson::Value &, BOOL *) = + ^NSString *(const flowjson::Value &endpoints, BOOL *stop) { + for (const auto &endpoint : endpoints.elements()) { + NSString *base = PushyNormalizeEndpointBase( + PushyFromStdString(endpoint.AsString())); + if (base.length == 0 || [tried containsObject:base]) { + continue; + } + NSTimeInterval timeout = 0; + if (!takeAttempt(&timeout)) { + *stop = YES; + return nil; + } + [tried addObject:base]; + NSString *response = PushyHttpRequest( + [NSString stringWithFormat:@"%@%@%@", base, RCTPushyQueryPath(), appKey], + @"POST", body, timeout); + if (PushyIsValidResponse(response)) { + return response; + } } + return nil; + }; + + BOOL stop = NO; + double sample = arc4random() / 4294967296.0; + NSString *response = queryEndpoints( + flowcore::OrderEndpointCandidates(config.Get("endpoints"), sample), &stop); + if (response != nil || stop) { + return response; } for (const auto &queryUrl : config.Get("queryUrls").elements()) { NSString *listUrl = PushyFromStdString(queryUrl.AsString()); if (listUrl == nil) { continue; } - if (httpAttempts++ >= maxHttpAttempts) { - return nil; - } - NSTimeInterval listTimeout = PushyQueryRequestTimeout(deadlineUptime); - if (listTimeout <= 0) { + NSTimeInterval listTimeout = 0; + if (!takeAttempt(&listTimeout)) { return nil; } NSString *listText = PushyHttpRequest(listUrl, @"GET", nil, listTimeout); @@ -2972,28 +3400,8 @@ + (NSString *)runQueryRequest:(const flowjson::Value &)config if (!ok || !remote.IsArray()) { continue; } - for (const auto &endpoint : remote.elements()) { - NSString *base = PushyNormalizeEndpointBase( - PushyFromStdString(endpoint.AsString())); - if (base.length == 0 || [tried containsObject:base]) { - continue; - } - if (httpAttempts++ >= maxHttpAttempts) { - return nil; - } - [tried addObject:base]; - NSTimeInterval timeout = PushyQueryRequestTimeout(deadlineUptime); - if (timeout <= 0) { - return nil; - } - NSString *response = PushyHttpRequest( - [NSString stringWithFormat:@"%@%@%@", base, RCTPushyQueryPath(), appKey], - @"POST", body, timeout); - if (PushyIsValidResponse(response)) { - return response; - } - } - break; // one successfully fetched remote list is enough + // One successfully fetched remote list is enough. + return queryEndpoints(remote, &stop); } return nil; } @@ -3040,7 +3448,7 @@ + (BOOL)performAttempts:(const flowjson::Value &)attempts } NSMutableDictionary *options = [@{ - PushyOptionUrlKey: url, + PushyOptionUrlKey(): url, @"hash": hash, @"deadlineUptime": @(deadline), } mutableCopy]; diff --git a/ios/RCTPushy/RCTPushyConfiguration.mm b/ios/RCTPushy/RCTPushyConfiguration.mm index 2714837d..793bbbca 100644 --- a/ios/RCTPushy/RCTPushyConfiguration.mm +++ b/ios/RCTPushy/RCTPushyConfiguration.mm @@ -6,14 +6,23 @@ NSString *RCTPushyRevealText(const char *hex) { size_t length = strlen(hex) / 2; - NSMutableString *text = [NSMutableString stringWithCapacity:length]; + unsigned char *bytes = (unsigned char *)malloc(length > 0 ? length : 1); + if (bytes == NULL) { + return @""; + } unsigned char base = PushyTextKeyBase; for (size_t i = 0; i < length; i++) { char pair[3] = {hex[i * 2], hex[i * 2 + 1], 0}; unsigned char byte = (unsigned char)strtoul(pair, NULL, 16); - [text appendFormat:@"%c", (char)(byte ^ (unsigned char)(base + 0x1D * i))]; + bytes[i] = byte ^ (unsigned char)(base + 0x1D * i); } - return text; + // One decode into a byte buffer (the texts are ASCII, see + // scripts/encode-native-text.ts) instead of a format call per character. + NSString *text = [[NSString alloc] initWithBytes:bytes + length:length + encoding:NSUTF8StringEncoding]; + free(bytes); + return text ?: @""; } NSString *RCTPushyQueryPath(void) { @@ -47,9 +56,13 @@ static void PushyConfigInvalid(NSString *message) { name, base ? @"a non-empty" : @"an"]); } NSMutableArray *result = [NSMutableArray array]; - NSRegularExpression *authority = [NSRegularExpression regularExpressionWithPattern: - @"^https?://(\\[[0-9a-fA-F:]+\\]|[a-zA-Z0-9.-]+)(:[0-9]+)?([/?#]|$)" - options:0 error:nil]; + static NSRegularExpression *authority; + static dispatch_once_t once; + dispatch_once(&once, ^{ + authority = [NSRegularExpression regularExpressionWithPattern: + @"^https?://(\\[[0-9a-fA-F:]+\\]|[a-zA-Z0-9.-]+)(:[0-9]+)?([/?#]|$)" + options:0 error:nil]; + }); for (id item in value) { NSString *url = [PushyConfigString(item, name, NO) stringByTrimmingCharactersInSet: NSCharacterSet.whitespaceAndNewlineCharacterSet]; diff --git a/react-native-update.podspec b/react-native-update.podspec index 8b48e5aa..0ecc4fea 100644 --- a/react-native-update.podspec +++ b/react-native-update.podspec @@ -59,10 +59,21 @@ Pod::Spec.new do |s| # anything below 15.0 there, so a hard-coded 11.0 fails the build. final_ios_deployment_target = defined?(min_ios_version_supported) ? min_ios_version_supported : '11.0' + # tvOS has its own version line: react-native-tvos reports it through + # min_supported_versions[:tvos] (stock React Native has no :tvos entry). + # 15.1 is react-native-tvos's own floor since 0.76, and clears the same + # Xcode 27 resource-bundle minimum as iOS. + final_tvos_deployment_target = begin + defined?(min_supported_versions) ? min_supported_versions[:tvos] : nil + rescue StandardError + nil + end + final_tvos_deployment_target ||= '15.1' if valid_expo_project # --- Try to find and parse ExpoModulesCore.podspec only if it's an Expo project --- parsed_expo_ios_target = nil + parsed_expo_tvos_target = nil expo_modules_core_podspec_path = begin package_json_path = `node -p "#{expo_modules_core_package_json_js}"`.strip File.join(File.dirname(package_json_path), 'ExpoModulesCore.podspec') if $?.success? && package_json_path && !package_json_path.empty? @@ -76,6 +87,8 @@ Pod::Spec.new do |s| match = content.match(/s\.platforms\s*=\s*\{[\s\S]*?:ios\s*=>\s*'([^\']+)'/) # Match within s.platforms hash if match && match[1] parsed_expo_ios_target = match[1] + tvos_match = content.match(/s\.platforms\s*=\s*\{[\s\S]*?:tvos\s*=>\s*'([^\']+)'/) + parsed_expo_tvos_target = tvos_match[1] if tvos_match else match = content.match(/s\.platform\s*=\s*:ios,\s*'([^\']+)'/) # Fallback to s.platform = :ios, 'version' if match && match[1] @@ -89,12 +102,15 @@ Pod::Spec.new do |s| if parsed_expo_ios_target final_ios_deployment_target = parsed_expo_ios_target end + if parsed_expo_tvos_target + final_tvos_deployment_target = parsed_expo_tvos_target + end end # tvOS (react-native-tvos) shares the iOS sources; the only platform # differences live behind TARGET_OS_TV in RCTPushy.mm (download directory, # purged-cache recovery). - s.platforms = { :ios => final_ios_deployment_target, :tvos => final_ios_deployment_target } + s.platforms = { :ios => final_ios_deployment_target, :tvos => final_tvos_deployment_target } s.name = package['name'] s.version = package['version'] @@ -109,6 +125,9 @@ Pod::Spec.new do |s| s.source = { :git => 'https://github.com/reactnativecn/react-native-update.git', :tag => "#{s.version}" } s.libraries = 'bz2', 'z' + # tvOS checks for a network path before blocking a launch on the + # purged-version restore (nw_path_monitor). + s.tvos.frameworks = 'Network' # No search path into the pod source: every include under ios/ resolves # relative to the including file (the cpp/ core via ../../cpp/...), and the # former absolute "#{podspec_dir}/ios" entry made the generated xcconfig diff --git a/scripts/tests/ios-purge-restore/README.md b/scripts/tests/ios-purge-restore/README.md index b3718f8e..7bdb5928 100644 --- a/scripts/tests/ios-purge-restore/README.md +++ b/scripts/tests/ios-purge-restore/README.md @@ -39,8 +39,9 @@ or compilation instead of silently testing a stale copy. The host replaces only collaborators: React Native export/logging plumbing, application paths/configuration, defaults storage, network round delivery, -cold-start scheduling, the launch wait's return value/completion delivery, and -post-reset filesystem cleanup. Bundle-existence checks use real temporary files. +cold-start scheduling, the launch wait's return value/completion delivery, the +time elapsed since process start, the network-path probe, and post-reset +filesystem cleanup. Bundle-existence checks use real temporary files. The production state lock is real; every defaults mutation asserts ownership of that same `os_unfair_lock`. @@ -77,6 +78,13 @@ preferences domain. | `reset_wins` | Pause reset inside the real state lock; start stale commit; release reset | Old generation rejects **all** commit writes: no current/last version, version metadata or response cache; install UUID survives | | `commit_wins` | Pause commit inside the real state lock; start reset; release commit | Earlier commit succeeds, then reset clears its state, metadata and cache; a delayed done signal cannot restore it | | `complete_in_time` | Commit and done signal both arrive before wait returns | Normal successful restore still launches B with first-load protection | +| `late_launch` | 4.5s already elapsed since process start | Round deadline and launch wait shrink to the remaining 7.5s; B still launches | +| `offline_skip` | Network-path probe reports no path | No window, no round, no wait; packaged bundle launches | +| `spent_budget_skip` | 10.5s already elapsed (under the 2s minimum left) | Same as offline: the restore is not attempted | + +`config_equivalence` additionally checks the cross-platform canonical config +equality (defaults, key order, trailing slashes, duplicates; `0` is not +`false`; unparseable is never equivalent). A real GCD worker captures the request generation and blocks at a controlled response boundary. Semaphores establish each ordering. Reset races are exercised @@ -84,7 +92,8 @@ in both legal lock orders, with one operation paused while it owns the lock and the competing operation started on another thread. Five-second waits are only fail-fast deadlock guards, never timing assumptions. The launch wait itself is injected, so the tests do not spend 12 seconds per case. They still assert that -production requests a 12-second (not 13-second) wait budget. +production requests exactly the 12-second budget minus the time already spent +since process start. ## Negative controls diff --git a/scripts/tests/ios-purge-restore/extract.py b/scripts/tests/ios-purge-restore/extract.py index b94a5c5e..c212bde7 100644 --- a/scripts/tests/ios-purge-restore/extract.py +++ b/scripts/tests/ios-purge-restore/extract.py @@ -73,16 +73,19 @@ def declaration(source: str, name: str) -> tuple[int, str]: 'legacyParamPackageVersion', 'legacyParamBuildTime', 'paramLastVersion', 'paramCurrentVersion', 'paramIsFirstTime', 'paramIsFirstLoadOk', 'keyUuid', 'keyHashInfo', 'keyFirstLoadMarked', 'keyRolledBackMarked', - 'KeyPackageUpdatedMarked', 'keyNativeCheckCache', 'BUNDLE_FILE_NAME', + 'keyNativeCheckCache', 'BUNDLE_FILE_NAME', 'pushyStateLock', 'ignoreRollback', 'pushyIsUsingBundleUrl', - 'pushyResetGeneration', 'pushyLaunchVersion', 'pushyCrashHoldActive', - 'pushyPurgeRestoreActive', 'pushyPurgeRestoreWindowOpen', - 'pushyHostRoundResult', 'kPushyPurgeRestoreBudget', + 'pushyResetGeneration', 'pushyNativeConfigGeneration', 'pushyLaunchVersion', + 'pushyCrashHoldActive', 'pushyPurgeRestoreActive', 'pushyPurgeRestoreWindowOpen', + 'pushyHostRoundResult', 'kPushyPurgeRestoreBudget', 'kPushyPurgeRestoreMinimumBudget', ] HELPERS = [ - 'PushyWithStateLock', 'PushyToStdString', 'PushyFromStdString', + 'PushyRevealOnce', 'KeyPackageUpdatedMarked', 'PushyWithStateLock', 'PushyToStdString', 'PushyFromStdString', 'PushySetNullableString', 'PushyHashInfoKey', 'PushyBinaryIdentityValue', 'PushyStateFromDefaults', 'PushyApplyStateToDefaults', 'PushySwitchVersionLocked', + # Canonical config equality (cross-platform contract), tested directly. + 'PushyNormalizeEndpointBase', 'PushyJsonIsBool', 'PushyJsonEqual', + 'PushyCanonicalUrlList', 'PushyCanonicalConfig', 'PushyConfigsEquivalent', ] MUTATIONS = ('late-activation', 'skip-reresolve', 'ignore-reset') diff --git a/scripts/tests/ios-purge-restore/purge_restore_test.mm b/scripts/tests/ios-purge-restore/purge_restore_test.mm index 184cae9a..f404fa3e 100644 --- a/scripts/tests/ios-purge-restore/purge_restore_test.mm +++ b/scripts/tests/ios-purge-restore/purge_restore_test.mm @@ -61,6 +61,9 @@ static void Await(dispatch_semaphore_t gate, const char *description) { static BOOL resetResolved; static int scheduleCount; static int64_t waitBudget; +static NSTimeInterval processElapsed; +static BOOL networkPath; +static NSTimeInterval ExpectedBudget(void) { return 12 - processElapsed; } static NSString *root; // In-memory collaborator, deliberately NOT an NSUserDefaults subclass: inherited @@ -171,6 +174,7 @@ + (void)scheduleFromColdStart:(NSString *)rolledBack; + (void)startRoundWithDeadline:(NSTimeInterval)deadline; + (BOOL)restorePurgedLaunch:(NSString *)purged rolledBack:(NSString *)rolledBack; + (BOOL)commitRoundWithGeneration:(uint64_t)generation + configGeneration:(uint64_t)configGeneration hashInfo:(NSDictionary *)hashInfo activate:(NSString *)hash responseText:(NSString *)response @@ -277,7 +281,7 @@ + (void)scheduleFromColdStart:(NSString *)rolledBack { (void)rolledBack; ++sched + (void)startRoundWithDeadline:(NSTimeInterval)deadline { @autoreleasepool { actor = Actor::Commit; - Expect(deadline == 100 + kPushyPurgeRestoreBudget, "round receives the launch deadline"); + Expect(deadline == 100 + ExpectedBudget(), "round receives the launch deadline"); requestGeneration = pushyResetGeneration.load(); Signal(requestStarted); Await(allowCommit, "release the native check response"); @@ -285,6 +289,7 @@ + (void)startRoundWithDeadline:(NSTimeInterval)deadline { // B represents an already-complete on-disk download. Do not reproduce // the activation policy here: the production commit must enforce it. roundCommitted = [self commitRoundWithGeneration:requestGeneration + configGeneration:pushyNativeConfigGeneration.load() hashInfo:@{@"hash": @"B", @"info": @{@"name": @"B", @"forceBootRescue": @YES}} activate:@"B" responseText:@"{\"update\":true,\"hash\":\"B\"}" request:@"request" config:@"config" responseAt:123 activated:&roundActivated]; @@ -293,6 +298,10 @@ + (void)startRoundWithDeadline:(NSTimeInterval)deadline { actor = Actor::None; } } +// Launch-budget collaborators: time already spent since process start and +// whether the system reports a network path. +static NSTimeInterval PushyProcessElapsed(void) { return processElapsed; } +static BOOL PushyHasNetworkPath(void) { return networkPath; } #define dispatch_semaphore_wait TestLaunchWait #define dispatch_semaphore_signal TestCompletionSignal #define dispatch_time TestLaunchDeadline @@ -323,6 +332,8 @@ static void SetUp(Order selected) { resetResolved = NO; scheduleCount = 0; waitBudget = 0; + processElapsed = 0; + networkPath = YES; testDefaults = [TestDefaults new]; engine = [RCTPushy new]; root = [NSTemporaryDirectory() stringByAppendingPathComponent:NSUUID.UUID.UUIDString]; @@ -335,6 +346,7 @@ static void SetUp(Order selected) { atomically:YES encoding:NSUTF8StringEncoding error:NULL], "create packaged fixture bundle"); PushyWithStateLock(^{ pushyResetGeneration.store(0); + pushyNativeConfigGeneration.store(0); ignoreRollback.store(false); pushyIsUsingBundleUrl.store(false); pushyLaunchVersion = nil; @@ -396,7 +408,8 @@ static void ExpectRestored(NSURL *url) { static void Finish(void) { if (order != Order::CompleteInTime) { Signal(allowSignal); } Await(workerSettled, "worker's delayed signal delivered before teardown"); - Expect(waitBudget == (int64_t)(12 * NSEC_PER_SEC), "launch wait must stay at 12 seconds, not 13"); + Expect(waitBudget == (int64_t)(ExpectedBudget() * NSEC_PER_SEC), + "launch wait must be 12 seconds minus the time since process start"); Expect(scheduleCount == 1, "bundleURL must retain its finally scheduling path"); Expect(!pushyPurgeRestoreWindowOpen, "every completed launch must close its restore window"); Expect([[NSFileManager defaultManager] removeItemAtPath:root error:NULL], "remove fixture files"); @@ -448,6 +461,31 @@ static void CompleteInTime(void) { Finish(); } +// Time spent before +bundleURL counts against the launch budget. +static void LateLaunch(void) { + SetUp(Order::CompleteInTime); + processElapsed = 4.5; + ExpectRestored([RCTPushy bundleURL]); + Expect(roundCommitted && roundActivated, "restore still runs on the remaining budget"); + Finish(); +} + +// No restore attempt (no wait, no round) when it cannot help: offline, or +// the launch budget is already spent. +static void Skipped(BOOL offline) { + SetUp(Order::CompleteInTime); + networkPath = !offline; + processElapsed = offline ? 0 : 10.5; + ExpectPackaged([RCTPushy bundleURL]); + Expect(waitBudget == 0, "skipped restore must not wait"); + Expect(scheduleCount == 1, "bundleURL must retain its finally scheduling path"); + Expect(!pushyPurgeRestoreWindowOpen && !pushyPurgeRestoreActive.load(), + "skipped restore must not open its window"); + Expect(dispatch_semaphore_wait(requestStarted, DISPATCH_TIME_NOW) != 0, + "skipped restore must not start the round"); + Expect([[NSFileManager defaultManager] removeItemAtPath:root error:NULL], "remove fixture files"); +} + // This contract test deliberately does not call SetUp: a new collaborator must // start empty on its own, not only because the fixture reset some known keys. static void DefaultsIsolation(void) { @@ -469,6 +507,33 @@ static void DefaultsIsolation(void) { }); } +// Canonical config equality shared with JS/Android/Harmony: equal canonical +// forms are the same config (persistConfiguration no-op, hasJsRound match). +static void ConfigEquivalence(void) { + struct Pair { NSString *a, *b; bool equivalent; const char *why; }; + const Pair pairs[] = { + {@"{\"appKey\":\"k\",\"endpoints\":[\"https://a/\",\"https://a\"]}", + @"{\"endpoints\":[\"https://a\"],\"appKey\":\"k\",\"disabled\":false,\"queryUrls\":[]," + @"\"rnu\":\"\",\"rn\":\"\",\"afterDownload\":\"none\"}", + true, "defaults, key order, trailing slash and duplicates are not changes"}, + {@"{\"appKey\":\"k\",\"queryUrls\":[\"https://q//\"]}", + @"{\"appKey\":\"k\",\"queryUrls\":[\"https://q\",\"https://q/\"]}", + true, "every trailing slash is stripped before deduplication"}, + {@"{\"appKey\":\"k\"}", @"{\"appKey\":\"j\"}", false, "a different appKey is a change"}, + {@"{\"appKey\":\"k\",\"disabled\":0}", @"{\"appKey\":\"k\"}", false, "0 is not false"}, + {@"{\"appKey\":\"k\",\"endpoints\":[\"https://a\",\"https://b\"]}", + @"{\"appKey\":\"k\",\"endpoints\":[\"https://b\",\"https://a\"]}", + false, "endpoint order is significant"}, + {@"[1]", @"[1]", false, "a non-object is never equivalent"}, + {@"{", @"{", false, "unparseable text is never equivalent, even to itself"}, + }; + for (const auto &pair : pairs) { + Expect(PushyConfigsEquivalent(pair.a, pair.b) == pair.equivalent, pair.why); + Expect(PushyConfigsEquivalent(pair.b, pair.a) == pair.equivalent, "equivalence is symmetric"); + } + Expect(!PushyConfigsEquivalent(nil, @"{\"appKey\":\"k\"}"), "no stored config is a change"); +} + int main(int argc, char **argv) { @autoreleasepool { // These intentional failures run in separate processes. Erasing the @@ -487,6 +552,9 @@ int main(int argc, char **argv) { testName = "defaults_isolation"; DefaultsIsolation(); std::printf("[PASS] defaults_isolation\n"); + testName = "config_equivalence"; + ConfigEquivalence(); + std::printf("[PASS] config_equivalence\n"); struct Case { const char *name; void (*run)(); }; const Case cases[] = { {"late_commit", LateCommit}, @@ -494,6 +562,9 @@ int main(int argc, char **argv) { {"reset_wins", [] { ResetOrdering(Order::ResetFirst); }}, {"commit_wins", [] { ResetOrdering(Order::CommitFirst); }}, {"complete_in_time", CompleteInTime}, + {"late_launch", LateLaunch}, + {"offline_skip", [] { Skipped(YES); }}, + {"spent_budget_skip", [] { Skipped(NO); }}, }; int ran = 0; for (const auto &test : cases) { From addbf66ef52221a8b51c2d5a1270ef047148b39a Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:24:28 +0800 Subject: [PATCH 10/21] chore: type-check against react-native 0.85 The root devDependency was still react-native 0.73 while the e2e app (and every supported host) runs 0.85; align react-native and @react-native/typescript-config with Example/e2etest (0.85.2). react and react-test-renderer stay on 18 for the bun:test render tests. Co-Authored-By: Claude Opus 5.5 --- bun.lock | 892 +++++++++------------------------------------------ package.json | 4 +- 2 files changed, 146 insertions(+), 750 deletions(-) diff --git a/bun.lock b/bun.lock index c8f5be7c..63083678 100644 --- a/bun.lock +++ b/bun.lock @@ -6,13 +6,13 @@ "name": "react-native-update", "devDependencies": { "@biomejs/biome": "^2.5.5", - "@react-native/typescript-config": "0.84.1", + "@react-native/typescript-config": "0.85.2", "@types/bun": "^1.3.11", "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-test-renderer": "18.0.7", "react": "18.2.0", - "react-native": "0.73", + "react-native": "0.85.2", "react-test-renderer": "18.2.0", "typescript": "^7.0.2", }, @@ -33,233 +33,31 @@ "@babel/generator": ["@babel/generator@7.27.0", "", { "dependencies": { "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" } }, "sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw=="], - "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g=="], - "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.0", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-validator-option": "^7.25.9", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA=="], - "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.27.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/helper-replace-supers": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/traverse": "^7.27.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg=="], - - "@babel/helper-create-regexp-features-plugin": ["@babel/helper-create-regexp-features-plugin@7.27.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ=="], - - "@babel/helper-define-polyfill-provider": ["@babel/helper-define-polyfill-provider@0.6.4", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", "@babel/helper-plugin-utils": "^7.22.5", "debug": "^4.1.1", "lodash.debounce": "^4.0.8", "resolve": "^1.14.2" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw=="], - - "@babel/helper-environment-visitor": ["@babel/helper-environment-visitor@7.24.7", "", { "dependencies": { "@babel/types": "^7.24.7" } }, "sha512-DoiN84+4Gnd0ncbBOM9AZENV4a5ZiL39HYMyZJGZ/AZEykHYdJw0wW3kdcsh9/Kn+BRXHLkkklZ51ecPKmI1CQ=="], - - "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ=="], + "@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="], "@babel/helper-module-imports": ["@babel/helper-module-imports@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw=="], "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.26.0", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw=="], - "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.25.9", "", { "dependencies": { "@babel/types": "^7.25.9" } }, "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ=="], - - "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.26.5", "", {}, "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg=="], + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="], - "@babel/helper-remap-async-to-generator": ["@babel/helper-remap-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-wrap-function": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw=="], - - "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.26.5", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.25.9", "@babel/helper-optimise-call-expression": "^7.25.9", "@babel/traverse": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg=="], - - "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.25.9", "", { "dependencies": { "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA=="], - - "@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="], "@babel/helper-validator-option": ["@babel/helper-validator-option@7.25.9", "", {}, "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw=="], - "@babel/helper-wrap-function": ["@babel/helper-wrap-function@7.25.9", "", { "dependencies": { "@babel/template": "^7.25.9", "@babel/traverse": "^7.25.9", "@babel/types": "^7.25.9" } }, "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g=="], - "@babel/helpers": ["@babel/helpers@7.27.0", "", { "dependencies": { "@babel/template": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg=="], - "@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], - - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": ["@babel/plugin-bugfix-firefox-class-in-computed-class-key@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g=="], - - "@babel/plugin-bugfix-safari-class-field-initializer-scope": ["@babel/plugin-bugfix-safari-class-field-initializer-scope@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw=="], - - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": ["@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug=="], - - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": ["@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-transform-optional-chaining": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.13.0" } }, "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g=="], - - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": ["@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg=="], - - "@babel/plugin-proposal-async-generator-functions": ["@babel/plugin-proposal-async-generator-functions@7.20.7", "", { "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-remap-async-to-generator": "^7.18.9", "@babel/plugin-syntax-async-generators": "^7.8.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA=="], - - "@babel/plugin-proposal-class-properties": ["@babel/plugin-proposal-class-properties@7.18.6", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ=="], - - "@babel/plugin-proposal-export-default-from": ["@babel/plugin-proposal-export-default-from@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ykqgwNfSnNOB+C8fV5X4mG3AVmvu+WVxcaU9xHHtBb7PCrPeweMmPjGsn8eMaeJg6SJuoUuZENeeSWaarWqonQ=="], - - "@babel/plugin-proposal-nullish-coalescing-operator": ["@babel/plugin-proposal-nullish-coalescing-operator@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA=="], - - "@babel/plugin-proposal-numeric-separator": ["@babel/plugin-proposal-numeric-separator@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-numeric-separator": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q=="], - - "@babel/plugin-proposal-object-rest-spread": ["@babel/plugin-proposal-object-rest-spread@7.20.7", "", { "dependencies": { "@babel/compat-data": "^7.20.5", "@babel/helper-compilation-targets": "^7.20.7", "@babel/helper-plugin-utils": "^7.20.2", "@babel/plugin-syntax-object-rest-spread": "^7.8.3", "@babel/plugin-transform-parameters": "^7.20.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-d2S98yCiLxDVmBmE8UjGcfPvNEUbA1U5q5WxaWFUGRzJSVAZqm5W6MbPct0jxnegUZ0niLeNX+IOzEs7wYg9Dg=="], - - "@babel/plugin-proposal-optional-catch-binding": ["@babel/plugin-proposal-optional-catch-binding@7.18.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.18.6", "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw=="], - - "@babel/plugin-proposal-optional-chaining": ["@babel/plugin-proposal-optional-chaining@7.21.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.20.2", "@babel/helper-skip-transparent-expression-wrappers": "^7.20.0", "@babel/plugin-syntax-optional-chaining": "^7.8.3" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-p4zeefM72gpmEe2fkUr/OnOXpWEf8nAgk7ZYVqqfFiyIG7oFfVZcCrU64hWn5xp4tQ9LkV4bTIa5rD0KANpKNA=="], - - "@babel/plugin-proposal-private-property-in-object": ["@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w=="], - - "@babel/plugin-syntax-async-generators": ["@babel/plugin-syntax-async-generators@7.8.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw=="], - - "@babel/plugin-syntax-dynamic-import": ["@babel/plugin-syntax-dynamic-import@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ=="], - - "@babel/plugin-syntax-export-default-from": ["@babel/plugin-syntax-export-default-from@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9MhJ/SMTsVqsd69GyQg89lYR4o9T+oDGv5F6IsigxxqFVOyR/IflDLYP8WDI1l8fkhNGGktqkvL5qwNCtGEpgQ=="], - - "@babel/plugin-syntax-flow": ["@babel/plugin-syntax-flow@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-B+O2DnPc0iG+YXFqOxv2WNuNU97ToWjOomUQ78DouOENWUaM5sVrmet9mcomUGQFwpJd//gvUagXBSdzO1fRKg=="], - - "@babel/plugin-syntax-import-assertions": ["@babel/plugin-syntax-import-assertions@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg=="], - - "@babel/plugin-syntax-import-attributes": ["@babel/plugin-syntax-import-attributes@7.26.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A=="], - - "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA=="], - - "@babel/plugin-syntax-nullish-coalescing-operator": ["@babel/plugin-syntax-nullish-coalescing-operator@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ=="], - - "@babel/plugin-syntax-numeric-separator": ["@babel/plugin-syntax-numeric-separator@7.10.4", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug=="], - - "@babel/plugin-syntax-object-rest-spread": ["@babel/plugin-syntax-object-rest-spread@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA=="], - - "@babel/plugin-syntax-optional-catch-binding": ["@babel/plugin-syntax-optional-catch-binding@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q=="], - - "@babel/plugin-syntax-optional-chaining": ["@babel/plugin-syntax-optional-chaining@7.8.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg=="], - - "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ=="], - - "@babel/plugin-syntax-unicode-sets-regex": ["@babel/plugin-syntax-unicode-sets-regex@7.18.6", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.18.6", "@babel/helper-plugin-utils": "^7.18.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg=="], - - "@babel/plugin-transform-arrow-functions": ["@babel/plugin-transform-arrow-functions@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg=="], - - "@babel/plugin-transform-async-generator-functions": ["@babel/plugin-transform-async-generator-functions@7.26.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-remap-async-to-generator": "^7.25.9", "@babel/traverse": "^7.26.8" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg=="], - - "@babel/plugin-transform-async-to-generator": ["@babel/plugin-transform-async-to-generator@7.25.9", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-remap-async-to-generator": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ=="], - - "@babel/plugin-transform-block-scoped-functions": ["@babel/plugin-transform-block-scoped-functions@7.26.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ=="], - - "@babel/plugin-transform-block-scoping": ["@babel/plugin-transform-block-scoping@7.27.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ=="], - - "@babel/plugin-transform-class-properties": ["@babel/plugin-transform-class-properties@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q=="], - - "@babel/plugin-transform-class-static-block": ["@babel/plugin-transform-class-static-block@7.26.0", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.12.0" } }, "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ=="], - - "@babel/plugin-transform-classes": ["@babel/plugin-transform-classes@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9", "@babel/traverse": "^7.25.9", "globals": "^11.1.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg=="], - - "@babel/plugin-transform-computed-properties": ["@babel/plugin-transform-computed-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/template": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA=="], - - "@babel/plugin-transform-destructuring": ["@babel/plugin-transform-destructuring@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ=="], - - "@babel/plugin-transform-dotall-regex": ["@babel/plugin-transform-dotall-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA=="], - - "@babel/plugin-transform-duplicate-keys": ["@babel/plugin-transform-duplicate-keys@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw=="], - - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": ["@babel/plugin-transform-duplicate-named-capturing-groups-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog=="], - - "@babel/plugin-transform-dynamic-import": ["@babel/plugin-transform-dynamic-import@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg=="], - - "@babel/plugin-transform-exponentiation-operator": ["@babel/plugin-transform-exponentiation-operator@7.26.3", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ=="], - - "@babel/plugin-transform-export-namespace-from": ["@babel/plugin-transform-export-namespace-from@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww=="], - - "@babel/plugin-transform-flow-strip-types": ["@babel/plugin-transform-flow-strip-types@7.26.5", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/plugin-syntax-flow": "^7.26.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-eGK26RsbIkYUns3Y8qKl362juDDYK+wEdPGHGrhzUl6CewZFo55VZ7hg+CyMFU4dd5QQakBN86nBMpRsFpRvbQ=="], - - "@babel/plugin-transform-for-of": ["@babel/plugin-transform-for-of@7.26.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg=="], - - "@babel/plugin-transform-function-name": ["@babel/plugin-transform-function-name@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA=="], - - "@babel/plugin-transform-json-strings": ["@babel/plugin-transform-json-strings@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw=="], - - "@babel/plugin-transform-literals": ["@babel/plugin-transform-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ=="], - - "@babel/plugin-transform-logical-assignment-operators": ["@babel/plugin-transform-logical-assignment-operators@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q=="], - - "@babel/plugin-transform-member-expression-literals": ["@babel/plugin-transform-member-expression-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA=="], - - "@babel/plugin-transform-modules-amd": ["@babel/plugin-transform-modules-amd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw=="], - - "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.26.3", "", { "dependencies": { "@babel/helper-module-transforms": "^7.26.0", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ=="], - - "@babel/plugin-transform-modules-systemjs": ["@babel/plugin-transform-modules-systemjs@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9", "@babel/traverse": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA=="], - - "@babel/plugin-transform-modules-umd": ["@babel/plugin-transform-modules-umd@7.25.9", "", { "dependencies": { "@babel/helper-module-transforms": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw=="], - - "@babel/plugin-transform-named-capturing-groups-regex": ["@babel/plugin-transform-named-capturing-groups-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA=="], - - "@babel/plugin-transform-new-target": ["@babel/plugin-transform-new-target@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ=="], - - "@babel/plugin-transform-nullish-coalescing-operator": ["@babel/plugin-transform-nullish-coalescing-operator@7.26.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw=="], - - "@babel/plugin-transform-numeric-separator": ["@babel/plugin-transform-numeric-separator@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q=="], - - "@babel/plugin-transform-object-rest-spread": ["@babel/plugin-transform-object-rest-spread@7.25.9", "", { "dependencies": { "@babel/helper-compilation-targets": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-transform-parameters": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg=="], - - "@babel/plugin-transform-object-super": ["@babel/plugin-transform-object-super@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-replace-supers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A=="], - - "@babel/plugin-transform-optional-catch-binding": ["@babel/plugin-transform-optional-catch-binding@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g=="], - - "@babel/plugin-transform-optional-chaining": ["@babel/plugin-transform-optional-chaining@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A=="], - - "@babel/plugin-transform-parameters": ["@babel/plugin-transform-parameters@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g=="], - - "@babel/plugin-transform-private-methods": ["@babel/plugin-transform-private-methods@7.25.9", "", { "dependencies": { "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw=="], - - "@babel/plugin-transform-private-property-in-object": ["@babel/plugin-transform-private-property-in-object@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw=="], - - "@babel/plugin-transform-property-literals": ["@babel/plugin-transform-property-literals@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA=="], - - "@babel/plugin-transform-react-display-name": ["@babel/plugin-transform-react-display-name@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-KJfMlYIUxQB1CJfO3e0+h0ZHWOTLCPP115Awhaz8U0Zpq36Gl/cXlpoyMRnUWlhNUBAzldnCiAZNvCDj7CrKxQ=="], - - "@babel/plugin-transform-react-jsx": ["@babel/plugin-transform-react-jsx@7.25.9", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/types": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s5XwpQYCqGerXl+Pu6VDL3x0j2d82eiV77UJ8a2mDHAW7j9SWRqQ2y1fNo1Z74CdcYipl5Z41zvjj4Nfzq36rw=="], - - "@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg=="], - - "@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg=="], - - "@babel/plugin-transform-regenerator": ["@babel/plugin-transform-regenerator@7.27.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "regenerator-transform": "^0.15.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA=="], - - "@babel/plugin-transform-regexp-modifiers": ["@babel/plugin-transform-regexp-modifiers@7.26.0", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw=="], - - "@babel/plugin-transform-reserved-words": ["@babel/plugin-transform-reserved-words@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg=="], - - "@babel/plugin-transform-runtime": ["@babel/plugin-transform-runtime@7.26.10", "", { "dependencies": { "@babel/helper-module-imports": "^7.25.9", "@babel/helper-plugin-utils": "^7.26.5", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-NWaL2qG6HRpONTnj4JvDU6th4jYeZOJgu3QhmFTCihib0ermtOJqktA5BduGm3suhhVe9EMP9c9+mfJ/I9slqw=="], - - "@babel/plugin-transform-shorthand-properties": ["@babel/plugin-transform-shorthand-properties@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng=="], - - "@babel/plugin-transform-spread": ["@babel/plugin-transform-spread@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A=="], - - "@babel/plugin-transform-sticky-regex": ["@babel/plugin-transform-sticky-regex@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA=="], - - "@babel/plugin-transform-template-literals": ["@babel/plugin-transform-template-literals@7.26.8", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q=="], - - "@babel/plugin-transform-typeof-symbol": ["@babel/plugin-transform-typeof-symbol@7.27.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w=="], - - "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.27.0", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", "@babel/helper-create-class-features-plugin": "^7.27.0", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", "@babel/plugin-syntax-typescript": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-fRGGjO2UEGPjvEcyAZXRXAS8AfdaQoq7HnxAbJoAoW10B9xOKesmmndJv+Sym2a+9FHWZ9KbyyLCe9s0Sn5jtg=="], - - "@babel/plugin-transform-unicode-escapes": ["@babel/plugin-transform-unicode-escapes@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q=="], - - "@babel/plugin-transform-unicode-property-regex": ["@babel/plugin-transform-unicode-property-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg=="], - - "@babel/plugin-transform-unicode-regex": ["@babel/plugin-transform-unicode-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA=="], - - "@babel/plugin-transform-unicode-sets-regex": ["@babel/plugin-transform-unicode-sets-regex@7.25.9", "", { "dependencies": { "@babel/helper-create-regexp-features-plugin": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ=="], - - "@babel/preset-env": ["@babel/preset-env@7.26.9", "", { "dependencies": { "@babel/compat-data": "^7.26.8", "@babel/helper-compilation-targets": "^7.26.5", "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", "@babel/plugin-syntax-import-assertions": "^7.26.0", "@babel/plugin-syntax-import-attributes": "^7.26.0", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", "@babel/plugin-transform-arrow-functions": "^7.25.9", "@babel/plugin-transform-async-generator-functions": "^7.26.8", "@babel/plugin-transform-async-to-generator": "^7.25.9", "@babel/plugin-transform-block-scoped-functions": "^7.26.5", "@babel/plugin-transform-block-scoping": "^7.25.9", "@babel/plugin-transform-class-properties": "^7.25.9", "@babel/plugin-transform-class-static-block": "^7.26.0", "@babel/plugin-transform-classes": "^7.25.9", "@babel/plugin-transform-computed-properties": "^7.25.9", "@babel/plugin-transform-destructuring": "^7.25.9", "@babel/plugin-transform-dotall-regex": "^7.25.9", "@babel/plugin-transform-duplicate-keys": "^7.25.9", "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-dynamic-import": "^7.25.9", "@babel/plugin-transform-exponentiation-operator": "^7.26.3", "@babel/plugin-transform-export-namespace-from": "^7.25.9", "@babel/plugin-transform-for-of": "^7.26.9", "@babel/plugin-transform-function-name": "^7.25.9", "@babel/plugin-transform-json-strings": "^7.25.9", "@babel/plugin-transform-literals": "^7.25.9", "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", "@babel/plugin-transform-member-expression-literals": "^7.25.9", "@babel/plugin-transform-modules-amd": "^7.25.9", "@babel/plugin-transform-modules-commonjs": "^7.26.3", "@babel/plugin-transform-modules-systemjs": "^7.25.9", "@babel/plugin-transform-modules-umd": "^7.25.9", "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", "@babel/plugin-transform-new-target": "^7.25.9", "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", "@babel/plugin-transform-numeric-separator": "^7.25.9", "@babel/plugin-transform-object-rest-spread": "^7.25.9", "@babel/plugin-transform-object-super": "^7.25.9", "@babel/plugin-transform-optional-catch-binding": "^7.25.9", "@babel/plugin-transform-optional-chaining": "^7.25.9", "@babel/plugin-transform-parameters": "^7.25.9", "@babel/plugin-transform-private-methods": "^7.25.9", "@babel/plugin-transform-private-property-in-object": "^7.25.9", "@babel/plugin-transform-property-literals": "^7.25.9", "@babel/plugin-transform-regenerator": "^7.25.9", "@babel/plugin-transform-regexp-modifiers": "^7.26.0", "@babel/plugin-transform-reserved-words": "^7.25.9", "@babel/plugin-transform-shorthand-properties": "^7.25.9", "@babel/plugin-transform-spread": "^7.25.9", "@babel/plugin-transform-sticky-regex": "^7.25.9", "@babel/plugin-transform-template-literals": "^7.26.8", "@babel/plugin-transform-typeof-symbol": "^7.26.7", "@babel/plugin-transform-unicode-escapes": "^7.25.9", "@babel/plugin-transform-unicode-property-regex": "^7.25.9", "@babel/plugin-transform-unicode-regex": "^7.25.9", "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ=="], - - "@babel/preset-flow": ["@babel/preset-flow@7.25.9", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.25.9", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-transform-flow-strip-types": "^7.25.9" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-EASHsAhE+SSlEzJ4bzfusnXSHiU+JfAYzj+jbw2vgQKgq5HrUr8qs+vgtiEL5dOH6sEweI+PNt2D7AqrDSHyqQ=="], - - "@babel/preset-modules": ["@babel/preset-modules@0.1.6-no-external-plugins", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@babel/types": "^7.4.4", "esutils": "^2.0.2" }, "peerDependencies": { "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA=="], - - "@babel/preset-typescript": ["@babel/preset-typescript@7.27.0", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.26.5", "@babel/helper-validator-option": "^7.25.9", "@babel/plugin-syntax-jsx": "^7.25.9", "@babel/plugin-transform-modules-commonjs": "^7.26.3", "@babel/plugin-transform-typescript": "^7.27.0" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-vxaPFfJtHhgeOVXRKuHpHPAOgymmy8V8I65T1q53R7GCZlefKeCaTyDs3zOPHTTbmquvNlQYC5klEvWsBAtrBQ=="], - - "@babel/register": ["@babel/register@7.25.9", "", { "dependencies": { "clone-deep": "^4.0.1", "find-cache-dir": "^2.0.0", "make-dir": "^2.1.0", "pirates": "^4.0.6", "source-map-support": "^0.5.16" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-8D43jXtGsYmEeDvm4MWHYUpWf8iiXgWYx3fW7E7Wb7Oe6FWqJPl5K6TuFW0dOwNZzEE5rjlaSJYH9JjrUKJszA=="], + "@babel/parser": ["@babel/parser@7.29.9", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA=="], "@babel/runtime": ["@babel/runtime@7.27.0", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw=="], "@babel/template": ["@babel/template@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/parser": "^7.27.0", "@babel/types": "^7.27.0" } }, "sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA=="], - "@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], + "@babel/traverse": ["@babel/traverse@7.29.10", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.9", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-B3lYAs2nkKskbgQ1nfy8Ma9ao9Q1rntTAQHrzzyDuj+TSO+X3le6PfWwqDvodsb+OumE10eMFfuMCTQSQ5iuIQ=="], - "@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], + "@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="], "@biomejs/biome": ["@biomejs/biome@2.5.5", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.5.5", "@biomejs/cli-darwin-x64": "2.5.5", "@biomejs/cli-linux-arm64": "2.5.5", "@biomejs/cli-linux-arm64-musl": "2.5.5", "@biomejs/cli-linux-x64": "2.5.5", "@biomejs/cli-linux-x64-musl": "2.5.5", "@biomejs/cli-win32-arm64": "2.5.5", "@biomejs/cli-win32-x64": "2.5.5" }, "bin": { "biome": "bin/biome" } }, "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ=="], @@ -279,18 +77,8 @@ "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.5.5", "", { "os": "win32", "cpu": "x64" }, "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g=="], - "@hapi/hoek": ["@hapi/hoek@9.3.0", "", {}, "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ=="], - - "@hapi/topo": ["@hapi/topo@5.1.0", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg=="], - "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], - "@jest/create-cache-key-function": ["@jest/create-cache-key-function@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3" } }, "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA=="], - - "@jest/environment": ["@jest/environment@29.7.0", "", { "dependencies": { "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0" } }, "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw=="], - - "@jest/fake-timers": ["@jest/fake-timers@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@sinonjs/fake-timers": "^10.0.2", "@types/node": "*", "jest-message-util": "^29.7.0", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ=="], - "@jest/schemas": ["@jest/schemas@29.6.3", "", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="], "@jest/types": ["@jest/types@29.6.3", "", { "dependencies": { "@jest/schemas": "^29.6.3", "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^17.0.8", "chalk": "^4.0.0" } }, "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw=="], @@ -307,68 +95,30 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], - "@react-native-community/cli": ["@react-native-community/cli@12.3.7", "", { "dependencies": { "@react-native-community/cli-clean": "12.3.7", "@react-native-community/cli-config": "12.3.7", "@react-native-community/cli-debugger-ui": "12.3.7", "@react-native-community/cli-doctor": "12.3.7", "@react-native-community/cli-hermes": "12.3.7", "@react-native-community/cli-plugin-metro": "12.3.7", "@react-native-community/cli-server-api": "12.3.7", "@react-native-community/cli-tools": "12.3.7", "@react-native-community/cli-types": "12.3.7", "chalk": "^4.1.2", "commander": "^9.4.1", "deepmerge": "^4.3.0", "execa": "^5.0.0", "find-up": "^4.1.0", "fs-extra": "^8.1.0", "graceful-fs": "^4.1.3", "prompts": "^2.4.2", "semver": "^7.5.2" }, "bin": { "react-native": "build/bin.js" } }, "sha512-7+mOhk+3+X3BjSJZZvYrDJynA00gPYTlvT28ZjiLlbuVGfqfNiBKaxuF7rty+gjjpch4iKGvLhIhSN5cuOsdHQ=="], - - "@react-native-community/cli-clean": ["@react-native-community/cli-clean@12.3.7", "", { "dependencies": { "@react-native-community/cli-tools": "12.3.7", "chalk": "^4.1.2", "execa": "^5.0.0" } }, "sha512-BCYW77QqyxfhiMEBOoHyciJRNV6Rhz1RvclReIKnCA9wAwmoJBeu4Mu+AwiECA2bUITX16fvPt3NwDsSd1jwfQ=="], - - "@react-native-community/cli-config": ["@react-native-community/cli-config@12.3.7", "", { "dependencies": { "@react-native-community/cli-tools": "12.3.7", "chalk": "^4.1.2", "cosmiconfig": "^5.1.0", "deepmerge": "^4.3.0", "glob": "^7.1.3", "joi": "^17.2.1" } }, "sha512-IU2UhO9yj1rEBNhHWGzIXpPDzha4hizLP/PUOrhR4BUf6RVPUWEp+e1PXNGR0qjIf6esu7OC7t6mLOhH0NUJEw=="], - - "@react-native-community/cli-debugger-ui": ["@react-native-community/cli-debugger-ui@12.3.7", "", { "dependencies": { "serve-static": "^1.13.1" } }, "sha512-UHUFrRdcjWSCdWG9KIp2QjuRIahBQnb9epnQI7JCq6NFbFHYfEI4rI7msjMn+gG8/tSwKTV2PTPuPmZ5wWlE7Q=="], - - "@react-native-community/cli-doctor": ["@react-native-community/cli-doctor@12.3.7", "", { "dependencies": { "@react-native-community/cli-config": "12.3.7", "@react-native-community/cli-platform-android": "12.3.7", "@react-native-community/cli-platform-ios": "12.3.7", "@react-native-community/cli-tools": "12.3.7", "chalk": "^4.1.2", "command-exists": "^1.2.8", "deepmerge": "^4.3.0", "envinfo": "^7.10.0", "execa": "^5.0.0", "hermes-profile-transformer": "^0.0.6", "node-stream-zip": "^1.9.1", "ora": "^5.4.1", "semver": "^7.5.2", "strip-ansi": "^5.2.0", "wcwidth": "^1.0.1", "yaml": "^2.2.1" } }, "sha512-gCamZztRoAyhciuQPqdz4Xe4t3gOdNsaADNd+rva+Rx8W2PoPeNv60i7/et06wlsn6B6Sh0/hMiAftJbiHDFkg=="], - - "@react-native-community/cli-hermes": ["@react-native-community/cli-hermes@12.3.7", "", { "dependencies": { "@react-native-community/cli-platform-android": "12.3.7", "@react-native-community/cli-tools": "12.3.7", "chalk": "^4.1.2", "hermes-profile-transformer": "^0.0.6" } }, "sha512-ezzeiSKjRXK2+i1AAe7NhhN9CEHrgtRmTn2MAdBpE++N8fH5EQZgxFcGgGdwGvns2fm9ivyyeVnI5eAYwvM+jg=="], - - "@react-native-community/cli-platform-android": ["@react-native-community/cli-platform-android@12.3.7", "", { "dependencies": { "@react-native-community/cli-tools": "12.3.7", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.2.4", "glob": "^7.1.3", "logkitty": "^0.7.1" } }, "sha512-mOltF3cpjNdJb3WSFwEHc1GH4ibCcnOvQ34OdWyblKy9ijuvG5SjNTlYR/UW/CURaDi3OUKAhxQMTY5d27bzGQ=="], - - "@react-native-community/cli-platform-ios": ["@react-native-community/cli-platform-ios@12.3.7", "", { "dependencies": { "@react-native-community/cli-tools": "12.3.7", "chalk": "^4.1.2", "execa": "^5.0.0", "fast-xml-parser": "^4.0.12", "glob": "^7.1.3", "ora": "^5.4.1" } }, "sha512-2WnVsMH4ORZIhBm/5nCms1NeeKG4KarNC7PMLmrXWXB/bibDcaNsjrJiqnmCUcpTEvTQTokRfoO7Aj6NM0Cqow=="], - - "@react-native-community/cli-plugin-metro": ["@react-native-community/cli-plugin-metro@12.3.7", "", {}, "sha512-ahEw0Vfnv2Nv/jdZ2QDuGjQ9l2SczO4lXjb3ubu5vEYNLyTw3jYsLMK6iES7YQ/ApQmKdG476HU1O9uZdpaYPg=="], - - "@react-native-community/cli-server-api": ["@react-native-community/cli-server-api@12.3.7", "", { "dependencies": { "@react-native-community/cli-debugger-ui": "12.3.7", "@react-native-community/cli-tools": "12.3.7", "compression": "^1.7.1", "connect": "^3.6.5", "errorhandler": "^1.5.1", "nocache": "^3.0.1", "pretty-format": "^26.6.2", "serve-static": "^1.13.1", "ws": "^7.5.1" } }, "sha512-LYETs3CCjrLn1ZU0kYv44TywiIl5IPFHZGeXhAh2TtgOk4mo3kvXxECDil9CdO3bmDra6qyiG61KHvzr8IrHdg=="], - - "@react-native-community/cli-tools": ["@react-native-community/cli-tools@12.3.7", "", { "dependencies": { "appdirsjs": "^1.2.4", "chalk": "^4.1.2", "find-up": "^5.0.0", "mime": "^2.4.1", "node-fetch": "^2.6.0", "open": "^6.2.0", "ora": "^5.4.1", "semver": "^7.5.2", "shell-quote": "^1.7.3", "sudo-prompt": "^9.0.0" } }, "sha512-7NL/1/i+wzd4fBr/FSr3ypR05tiU/Kv9l/M1sL1c6jfcDtWXAL90R161gQkQFK7shIQ8Idp0dQX1rq49tSyfQw=="], - - "@react-native-community/cli-types": ["@react-native-community/cli-types@12.3.7", "", { "dependencies": { "joi": "^17.2.1" } }, "sha512-NFtUMyIrNfi3A5C1cjVKDVvYHvvOF7MnOMwdD8jm2NQKewQJrehKBh1eMuykKdqhWyZmuemD4KKhL8f4FxgG0w=="], - - "@react-native/assets-registry": ["@react-native/assets-registry@0.73.1", "", {}, "sha512-2FgAbU7uKM5SbbW9QptPPZx8N9Ke2L7bsHb+EhAanZjFZunA9PaYtyjUQ1s7HD+zDVqOQIvjkpXSv7Kejd2tqg=="], - - "@react-native/babel-plugin-codegen": ["@react-native/babel-plugin-codegen@0.73.4", "", { "dependencies": { "@react-native/codegen": "0.73.3" } }, "sha512-XzRd8MJGo4Zc5KsphDHBYJzS1ryOHg8I2gOZDAUCGcwLFhdyGu1zBNDJYH2GFyDrInn9TzAbRIf3d4O+eltXQQ=="], - - "@react-native/babel-preset": ["@react-native/babel-preset@0.73.21", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/plugin-proposal-async-generator-functions": "^7.0.0", "@babel/plugin-proposal-class-properties": "^7.18.0", "@babel/plugin-proposal-export-default-from": "^7.0.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.0", "@babel/plugin-proposal-numeric-separator": "^7.0.0", "@babel/plugin-proposal-object-rest-spread": "^7.20.0", "@babel/plugin-proposal-optional-catch-binding": "^7.0.0", "@babel/plugin-proposal-optional-chaining": "^7.20.0", "@babel/plugin-syntax-dynamic-import": "^7.8.0", "@babel/plugin-syntax-export-default-from": "^7.0.0", "@babel/plugin-syntax-flow": "^7.18.0", "@babel/plugin-syntax-nullish-coalescing-operator": "^7.0.0", "@babel/plugin-syntax-optional-chaining": "^7.0.0", "@babel/plugin-transform-arrow-functions": "^7.0.0", "@babel/plugin-transform-async-to-generator": "^7.20.0", "@babel/plugin-transform-block-scoping": "^7.0.0", "@babel/plugin-transform-classes": "^7.0.0", "@babel/plugin-transform-computed-properties": "^7.0.0", "@babel/plugin-transform-destructuring": "^7.20.0", "@babel/plugin-transform-flow-strip-types": "^7.20.0", "@babel/plugin-transform-function-name": "^7.0.0", "@babel/plugin-transform-literals": "^7.0.0", "@babel/plugin-transform-modules-commonjs": "^7.0.0", "@babel/plugin-transform-named-capturing-groups-regex": "^7.0.0", "@babel/plugin-transform-parameters": "^7.0.0", "@babel/plugin-transform-private-methods": "^7.22.5", "@babel/plugin-transform-private-property-in-object": "^7.22.11", "@babel/plugin-transform-react-display-name": "^7.0.0", "@babel/plugin-transform-react-jsx": "^7.0.0", "@babel/plugin-transform-react-jsx-self": "^7.0.0", "@babel/plugin-transform-react-jsx-source": "^7.0.0", "@babel/plugin-transform-runtime": "^7.0.0", "@babel/plugin-transform-shorthand-properties": "^7.0.0", "@babel/plugin-transform-spread": "^7.0.0", "@babel/plugin-transform-sticky-regex": "^7.0.0", "@babel/plugin-transform-typescript": "^7.5.0", "@babel/plugin-transform-unicode-regex": "^7.0.0", "@babel/template": "^7.0.0", "@react-native/babel-plugin-codegen": "0.73.4", "babel-plugin-transform-flow-enums": "^0.0.2", "react-refresh": "^0.14.0" } }, "sha512-WlFttNnySKQMeujN09fRmrdWqh46QyJluM5jdtDNrkl/2Hx6N4XeDUGhABvConeK95OidVO7sFFf7sNebVXogA=="], - - "@react-native/codegen": ["@react-native/codegen@0.73.3", "", { "dependencies": { "@babel/parser": "^7.20.0", "flow-parser": "^0.206.0", "glob": "^7.1.1", "invariant": "^2.2.4", "jscodeshift": "^0.14.0", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" } }, "sha512-sxslCAAb8kM06vGy9Jyh4TtvjhcP36k/rvj2QE2Jdhdm61KvfafCATSIsOfc0QvnduWFcpXUPvAVyYwuv7PYDg=="], - - "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.73.18", "", { "dependencies": { "@react-native-community/cli-server-api": "12.3.7", "@react-native-community/cli-tools": "12.3.7", "@react-native/dev-middleware": "0.73.8", "@react-native/metro-babel-transformer": "0.73.15", "chalk": "^4.0.0", "execa": "^5.1.1", "metro": "^0.80.3", "metro-config": "^0.80.3", "metro-core": "^0.80.3", "node-fetch": "^2.2.0", "readline": "^1.3.0" } }, "sha512-RN8piDh/eF+QT6YYmrj3Zd9uiaDsRY/kMT0FYR42j8/M/boE4hs4Xn0u91XzT8CAkU9q/ilyo3wJsXIJo2teww=="], - - "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.73.3", "", {}, "sha512-RgEKnWuoo54dh7gQhV7kvzKhXZEhpF9LlMdZolyhGxHsBqZ2gXdibfDlfcARFFifPIiaZ3lXuOVVa4ei+uPgTw=="], + "@react-native/assets-registry": ["@react-native/assets-registry@0.85.2", "", {}, "sha512-kauC/oPaxklU4Y+u9gBfCBJm51qX6WBZq4xx0USCdimtp+G8+554kpygfSWIjoqCJa2o06bWxBEjesiuCv+LzA=="], - "@react-native/dev-middleware": ["@react-native/dev-middleware@0.73.8", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.73.3", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^1.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "node-fetch": "^2.2.0", "open": "^7.0.3", "serve-static": "^1.13.1", "temp-dir": "^2.0.0", "ws": "^6.2.2" } }, "sha512-oph4NamCIxkMfUL/fYtSsE+JbGOnrlawfQ0kKtDQ5xbOjPKotKoXqrs1eGwozNKv7FfQ393stk1by9a6DyASSg=="], + "@react-native/codegen": ["@react-native/codegen@0.85.2", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/parser": "^7.29.0", "hermes-parser": "0.33.3", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "tinyglobby": "^0.2.15", "yargs": "^17.6.2" } }, "sha512-XCginmxh0//++EXVOEJHBVZxHla294FzLCFF6jXwAUjvXVhqyIKyxhABfz+r4OOmaiuWk4Rtd4arqdAzeHeprg=="], - "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.73.5", "", {}, "sha512-Orrn8J/kqzEuXudl96XcZk84ZcdIpn1ojjwGSuaSQSXNcCYbOXyt0RwtW5kjCqjgSzGnOMsJNZc5FDXHVq/WzA=="], + "@react-native/community-cli-plugin": ["@react-native/community-cli-plugin@0.85.2", "", { "dependencies": { "@react-native/dev-middleware": "0.85.2", "debug": "^4.4.0", "invariant": "^2.2.4", "metro": "^0.84.0", "metro-config": "^0.84.0", "metro-core": "^0.84.0", "semver": "^7.1.3" }, "peerDependencies": { "@react-native-community/cli": "*", "@react-native/metro-config": "0.85.2" }, "optionalPeers": ["@react-native-community/cli", "@react-native/metro-config"] }, "sha512-3KLgSg1kHvBpr93zMaQhvfYTgnCw7yZRED+3J4dMcYjfSjtD0Wf8SofU6uBmAw9JaVYvP43lpdwUpI4p0+ABsg=="], - "@react-native/js-polyfills": ["@react-native/js-polyfills@0.73.1", "", {}, "sha512-ewMwGcumrilnF87H4jjrnvGZEaPFCAC4ebraEK+CurDDmwST/bIicI4hrOAv+0Z0F7DEK4O4H7r8q9vH7IbN4g=="], + "@react-native/debugger-frontend": ["@react-native/debugger-frontend@0.85.2", "", {}, "sha512-j+0b9H5f5hGTLQxHIhJU/b/W6ijuxJF+ZTLHB0se2kzUBNxFKd7DkIc6753qk3CJdiv55vxG3XDgmlpbHxOpmA=="], - "@react-native/metro-babel-transformer": ["@react-native/metro-babel-transformer@0.73.15", "", { "dependencies": { "@babel/core": "^7.20.0", "@react-native/babel-preset": "0.73.21", "hermes-parser": "0.15.0", "nullthrows": "^1.1.1" } }, "sha512-LlkSGaXCz+xdxc9819plmpsl4P4gZndoFtpjN3GMBIu6f7TBV0GVbyJAU4GE8fuAWPVSVL5ArOcdkWKSbI1klw=="], + "@react-native/debugger-shell": ["@react-native/debugger-shell@0.85.2", "", { "dependencies": { "cross-spawn": "^7.0.6", "debug": "^4.4.0", "fb-dotslash": "0.5.8" } }, "sha512-r5BkhqPMfg3LmaZS5zadHmBNVH5h4bhSpv4BEPGfK4gat9HABAMzUzybi+2wpgU3SoHxnyKGdExEJvoqVcjeRg=="], - "@react-native/normalize-colors": ["@react-native/normalize-colors@0.73.2", "", {}, "sha512-bRBcb2T+I88aG74LMVHaKms2p/T8aQd8+BZ7LuuzXlRfog1bMWWn/C5i0HVuvW4RPtXQYgIlGiXVDy9Ir1So/w=="], + "@react-native/dev-middleware": ["@react-native/dev-middleware@0.85.2", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "@react-native/debugger-frontend": "0.85.2", "@react-native/debugger-shell": "0.85.2", "chrome-launcher": "^0.15.2", "chromium-edge-launcher": "^0.3.0", "connect": "^3.6.5", "debug": "^4.4.0", "invariant": "^2.2.4", "nullthrows": "^1.1.1", "open": "^7.0.3", "serve-static": "^1.16.2", "ws": "^7.5.10" } }, "sha512-3J+NaDUg+QEfDeLAUzgaWhpaxEg78g+KwbydlDCewh2G6WnHpsty8XooruxNHzyAsqVWywZMrzmbn78Ctc1O9Q=="], - "@react-native/typescript-config": ["@react-native/typescript-config@0.84.1", "", {}, "sha512-ar7Gn6ma3b+Ricdxn2sTZL2DT1NMlrfsWmOkFZegpfQJzheqX/8gzIB1aIbfZyvhEDsoz07RG7wmsyQAWqXjsw=="], + "@react-native/gradle-plugin": ["@react-native/gradle-plugin@0.85.2", "", {}, "sha512-YXBOLeAqFrv7XwUeBPTKZeOV1FIxn4AW7UAEitScf3ibC8bu8+6NpJu4HWgbNQHg7vDbbTZVbcOl8EwGxsSq2w=="], - "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.73.4", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "react-native": "*" } }, "sha512-HpmLg1FrEiDtrtAbXiwCgXFYyloK/dOIPIuWW3fsqukwJEWAiTzm1nXGJ7xPU5XTHiWZ4sKup5Ebaj8z7iyWog=="], + "@react-native/js-polyfills": ["@react-native/js-polyfills@0.85.2", "", {}, "sha512-esGEAmKVM40DV/yVmNljCKZTIeUo7qXqc+Hwffkv3TG+b3E24xyFovHrbP98gGxZr2ZsEyx+2sKLdXF5asY5nw=="], - "@sideway/address": ["@sideway/address@4.1.5", "", { "dependencies": { "@hapi/hoek": "^9.0.0" } }, "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q=="], + "@react-native/normalize-colors": ["@react-native/normalize-colors@0.85.2", "", {}, "sha512-svuOLtjbFGXDdHsriHXuND5FgHg7XlkOXCbH/8+X4t76YLH6qSTffSIQQrKLDL5mn4EFU+Oh/PNO0/FfpnTOTg=="], - "@sideway/formula": ["@sideway/formula@3.0.1", "", {}, "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg=="], + "@react-native/typescript-config": ["@react-native/typescript-config@0.85.2", "", {}, "sha512-kJAIGIQdYSf2llBq28EuEjgkK0ELaOLs9hzZunhwQUWICqUuad4yJtqL1JioFeFrkzN3NvdfgM7eBN4Se4Tq9w=="], - "@sideway/pinpoint": ["@sideway/pinpoint@2.0.0", "", {}, "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ=="], + "@react-native/virtualized-lists": ["@react-native/virtualized-lists@0.85.2", "", { "dependencies": { "invariant": "^2.2.4", "nullthrows": "^1.1.1" }, "peerDependencies": { "@types/react": "^19.2.0", "react": "*", "react-native": "0.85.2" }, "optionalPeers": ["@types/react"] }, "sha512-wmVKpAlcr+UB0L5SpbrV865EdleUP7I5+X+48e1aRsQK8q+wsTRBXeUwWVip/1l+HZwlZFeO8iOILJ16VRu0Cw=="], "@sinclair/typebox": ["@sinclair/typebox@0.27.8", "", {}, "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA=="], - "@sinonjs/commons": ["@sinonjs/commons@3.0.1", "", { "dependencies": { "type-detect": "4.0.8" } }, "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ=="], - - "@sinonjs/fake-timers": ["@sinonjs/fake-timers@10.3.0", "", { "dependencies": { "@sinonjs/commons": "^3.0.0" } }, "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], "@types/istanbul-lib-coverage": ["@types/istanbul-lib-coverage@2.0.6", "", {}, "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w=="], @@ -383,8 +133,6 @@ "@types/react-test-renderer": ["@types/react-test-renderer@18.0.7", "", { "dependencies": { "@types/react": "*" } }, "sha512-1+ANPOWc6rB3IkSnElhjv6VLlKg2dSv/OWClUyZimbLsQyBn8Js9Vtdsi3UICJ2rIQ3k2la06dkB+C92QfhKmg=="], - "@types/stack-utils": ["@types/stack-utils@2.0.3", "", {}, "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw=="], - "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], "@types/yargs-parser": ["@types/yargs-parser@21.0.3", "", {}, "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ=="], @@ -431,70 +179,34 @@ "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], - "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.14.1", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="], - "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ansi-fragments": ["ansi-fragments@0.2.1", "", { "dependencies": { "colorette": "^1.0.7", "slice-ansi": "^2.0.0", "strip-ansi": "^5.0.0" } }, "sha512-DykbNHxuXQwUDRv5ibc2b0x7uw7wmwOGLBUd5RmaQ5z8Lhx19vwvKV+FAsM5rEA6dEcHxX+/Ad5s9eF2k2bB+w=="], + "anser": ["anser@1.4.10", "", {}, "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww=="], "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], - - "appdirsjs": ["appdirsjs@1.2.7", "", {}, "sha512-Quji6+8kLBC3NnBeo14nPDq0+2jUs5s3/xEye+udFHumHhRk4M7aAMXp/PBJqkKYGuuyR9M/6Dq7d2AViiGmhw=="], - - "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + "ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], "asap": ["asap@2.0.6", "", {}, "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA=="], - "ast-types": ["ast-types@0.15.2", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg=="], - - "astral-regex": ["astral-regex@1.0.0", "", {}, "sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg=="], - - "async-limiter": ["async-limiter@1.0.1", "", {}, "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ=="], - - "babel-core": ["babel-core@7.0.0-bridge.0", "", { "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg=="], - - "babel-plugin-polyfill-corejs2": ["babel-plugin-polyfill-corejs2@0.4.13", "", { "dependencies": { "@babel/compat-data": "^7.22.6", "@babel/helper-define-polyfill-provider": "^0.6.4", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g=="], - - "babel-plugin-polyfill-corejs3": ["babel-plugin-polyfill-corejs3@0.11.1", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.3", "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ=="], - - "babel-plugin-polyfill-regenerator": ["babel-plugin-polyfill-regenerator@0.6.4", "", { "dependencies": { "@babel/helper-define-polyfill-provider": "^0.6.4" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw=="], - - "babel-plugin-transform-flow-enums": ["babel-plugin-transform-flow-enums@0.0.2", "", { "dependencies": { "@babel/plugin-syntax-flow": "^7.12.1" } }, "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ=="], - - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "babel-plugin-syntax-hermes-parser": ["babel-plugin-syntax-hermes-parser@0.33.3", "", { "dependencies": { "hermes-parser": "0.33.3" } }, "sha512-/Z9xYdaJ1lC0pT9do6TqCqhOSLfZ5Ot8D5za1p+feEfWYupCOfGbhhEXN9r2ZgJtDNUNRw/Z+T2CvAGKBqtqWA=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], "browserslist": ["browserslist@4.24.4", "", { "dependencies": { "caniuse-lite": "^1.0.30001688", "electron-to-chromium": "^1.5.73", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" } }, "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A=="], "bser": ["bser@2.1.1", "", { "dependencies": { "node-int64": "^0.4.0" } }, "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ=="], - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], - "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], - "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], - - "caller-callsite": ["caller-callsite@2.0.0", "", { "dependencies": { "callsites": "^2.0.0" } }, "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ=="], - - "caller-path": ["caller-path@2.0.0", "", { "dependencies": { "caller-callsite": "^2.0.0" } }, "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A=="], - - "callsites": ["callsites@2.0.0", "", {}, "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ=="], - "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], "caniuse-lite": ["caniuse-lite@1.0.30001715", "", {}, "sha512-7ptkFGMm2OAOgvZpwgA4yjQ5SQbrNVGdRjzH0pBdy1Fasvcr+KAeECmbCAECzTuDuoX0FCY8KzUxjf9+9kfZEw=="], @@ -503,68 +215,32 @@ "chrome-launcher": ["chrome-launcher@0.15.2", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0" }, "bin": { "print-chrome-path": "bin/print-chrome-path.js" } }, "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ=="], - "chromium-edge-launcher": ["chromium-edge-launcher@1.0.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-pgtgjNKZ7i5U++1g1PWv75umkHvhVTDOQIZ+sjeUX9483S7Y6MUvO0lrd7ShGlQlFHMN4SwKTCq/X8hWrbv2KA=="], + "chromium-edge-launcher": ["chromium-edge-launcher@0.3.0", "", { "dependencies": { "@types/node": "*", "escape-string-regexp": "^4.0.0", "is-wsl": "^2.2.0", "lighthouse-logger": "^1.0.0", "mkdirp": "^1.0.4" } }, "sha512-p03azHlGjtyRvFEee3cyvtsRYdniSkwjkzmM/KmVnqT5d7QkkwpJBhis/zCLMYdQMVJ5tt140TBNqqrZPaWeFA=="], "ci-info": ["ci-info@2.0.0", "", {}, "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ=="], - "cli-cursor": ["cli-cursor@3.1.0", "", { "dependencies": { "restore-cursor": "^3.1.0" } }, "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw=="], - - "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "clone": ["clone@1.0.4", "", {}, "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg=="], - - "clone-deep": ["clone-deep@4.0.1", "", { "dependencies": { "is-plain-object": "^2.0.4", "kind-of": "^6.0.2", "shallow-clone": "^3.0.0" } }, "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "colorette": ["colorette@1.4.0", "", {}, "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g=="], - - "command-exists": ["command-exists@1.2.9", "", {}, "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w=="], - - "commander": ["commander@9.5.0", "", {}, "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ=="], - - "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="], - - "compressible": ["compressible@2.0.18", "", { "dependencies": { "mime-db": ">= 1.43.0 < 2" } }, "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg=="], - - "compression": ["compression@1.8.0", "", { "dependencies": { "bytes": "3.1.2", "compressible": "~2.0.18", "debug": "2.6.9", "negotiator": "~0.6.4", "on-headers": "~1.0.2", "safe-buffer": "5.2.1", "vary": "~1.1.2" } }, "sha512-k6WLKfunuqCYD3t6AsuPGvQWaKwuLLh2/xHNcX4qE+vIfDNXpSqnrhwA7O53R7WVQUnt8dVAIW+YHr7xTgOgGA=="], - - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], "connect": ["connect@3.7.0", "", { "dependencies": { "debug": "2.6.9", "finalhandler": "1.1.2", "parseurl": "~1.3.3", "utils-merge": "1.0.1" } }, "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ=="], - "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], - - "core-js-compat": ["core-js-compat@3.41.0", "", { "dependencies": { "browserslist": "^4.24.4" } }, "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A=="], + "content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], - "core-util-is": ["core-util-is@1.0.3", "", {}, "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="], - - "cosmiconfig": ["cosmiconfig@5.2.1", "", { "dependencies": { "import-fresh": "^2.0.0", "is-directory": "^0.3.1", "js-yaml": "^3.13.1", "parse-json": "^4.0.0" } }, "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA=="], + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], - "dayjs": ["dayjs@1.11.13", "", {}, "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="], - - "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - - "decamelize": ["decamelize@1.2.0", "", {}, "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA=="], - - "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="], - - "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], - - "denodeify": ["denodeify@1.2.1", "", {}, "sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg=="], + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], - "deprecated-react-native-prop-types": ["deprecated-react-native-prop-types@5.0.0", "", { "dependencies": { "@react-native/normalize-colors": "^0.73.0", "invariant": "^2.2.4", "prop-types": "^15.8.1" } }, "sha512-cIK8KYiiGVOFsKdPMmm1L3tA/Gl+JopXL6F5+C7x39MyPsQYnP57Im/D6bNUzcborD7fcMwiwZqcBdBXXZucYQ=="], - "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], @@ -575,184 +251,94 @@ "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "envinfo": ["envinfo@7.14.0", "", { "bin": { "envinfo": "dist/cli.js" } }, "sha512-CO40UI41xDQzhLB1hWyqUKgFhs250pNcGbyGKe1l/e4FSaI/+YE4IMG76GDt0In67WLPACIITC+sOi08x4wIvg=="], - - "error-ex": ["error-ex@1.3.2", "", { "dependencies": { "is-arrayish": "^0.2.1" } }, "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g=="], - "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], - "errorhandler": ["errorhandler@1.5.1", "", { "dependencies": { "accepts": "~1.3.7", "escape-html": "~1.0.3" } }, "sha512-rcOwbfvP1WTViVoUjcfZicVzjhjTuhSMntHh6mW3IrEiyE6mJyXvsToJUJGlGlw/2xU9P5whlWNGlIDVeCiT4A=="], - "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], - "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], - "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "exponential-backoff": ["exponential-backoff@3.1.2", "", {}, "sha512-8QxYTVXUkuy7fIIoitQkPwGonB8F3Zj8eEO8Sqg9Zv/bkI7RJAzowee4gr81Hak/dUTpA2Z7VfQgoijjPNlUZA=="], - "fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="], + "fb-dotslash": ["fb-dotslash@0.5.8", "", { "bin": { "dotslash": "bin/dotslash" } }, "sha512-XHYLKk9J4BupDxi9bSEhkfss0m+Vr9ChTrjhf9l2iw3jB5C7BnY4GVPoMcqbrTutsKJso6yj2nAB6BI/F2oZaA=="], "fb-watchman": ["fb-watchman@2.0.2", "", { "dependencies": { "bser": "2.1.1" } }, "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], "finalhandler": ["finalhandler@1.1.2", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "on-finished": "~2.3.0", "parseurl": "~1.3.3", "statuses": "~1.5.0", "unpipe": "~1.0.0" } }, "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA=="], - "find-cache-dir": ["find-cache-dir@2.1.0", "", { "dependencies": { "commondir": "^1.0.1", "make-dir": "^2.0.0", "pkg-dir": "^3.0.0" } }, "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ=="], - - "find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], - "flow-enums-runtime": ["flow-enums-runtime@0.0.6", "", {}, "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw=="], - "flow-parser": ["flow-parser@0.206.0", "", {}, "sha512-HVzoK3r6Vsg+lKvlIZzaWNBVai+FXTX1wdYhz/wVlH13tb/gOdLXmlTqy6odmTBhT5UoWUbq0k8263Qhr9d88w=="], - "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], - "fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="], - - "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], - - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], - - "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], - "globals": ["globals@11.12.0", "", {}, "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + "hermes-compiler": ["hermes-compiler@250829098.0.10", "", {}, "sha512-TcRlZ0/TlyfJqquRFAWoyElVNnkdYRi/sEp4/Qy8/GYxjg8j2cS9D4MjuaQ+qimkmLN7AmO+44IznRf06mAr0w=="], - "hermes-estree": ["hermes-estree@0.15.0", "", {}, "sha512-lLYvAd+6BnOqWdnNbP/Q8xfl8LOGw4wVjfrNd9Gt8eoFzhNBRVD95n4l2ksfMVOoxuVyegs85g83KS9QOsxbVQ=="], + "hermes-estree": ["hermes-estree@0.33.3", "", {}, "sha512-6kzYZHCk8Fy1Uc+t3HGYyJn3OL4aeqKLTyina4UFtWl8I0kSL7OmKThaiX+Uh2f8nGw3mo4Ifxg0M5Zk3/Oeqg=="], - "hermes-parser": ["hermes-parser@0.15.0", "", { "dependencies": { "hermes-estree": "0.15.0" } }, "sha512-Q1uks5rjZlE9RjMMjSUCkGrEIPI5pKJILeCtK1VmTj7U4pf3wVPoo+cxfu+s4cBAPy2JzikIIdCZgBoR6x7U1Q=="], - - "hermes-profile-transformer": ["hermes-profile-transformer@0.0.6", "", { "dependencies": { "source-map": "^0.7.3" } }, "sha512-cnN7bQUm65UWOy6cbGcCcZ3rpwW8Q/j4OP5aWRhEry4Z2t2aR1cjrbp0BS+KiBN0smvP1caBgAuxutvyvJILzQ=="], + "hermes-parser": ["hermes-parser@0.33.3", "", { "dependencies": { "hermes-estree": "0.33.3" } }, "sha512-Yg3HgaG4CqgyowtYjX/FsnPAuZdHOqSMtnbpylbptsQ9nwwSKsy6uRWcGO5RK0EqiX12q8HvDWKgeAVajRO5DA=="], "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], - "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "image-size": ["image-size@1.2.1", "", { "dependencies": { "queue": "6.0.2" }, "bin": { "image-size": "bin/image-size.js" } }, "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw=="], - - "import-fresh": ["import-fresh@2.0.0", "", { "dependencies": { "caller-path": "^2.0.0", "resolve-from": "^3.0.0" } }, "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg=="], - - "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], - - "inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], "invariant": ["invariant@2.2.4", "", { "dependencies": { "loose-envify": "^1.0.0" } }, "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA=="], - "is-arrayish": ["is-arrayish@0.2.1", "", {}, "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg=="], - - "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], - - "is-directory": ["is-directory@0.3.1", "", {}, "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw=="], - "is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-interactive": ["is-interactive@1.0.0", "", {}, "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-plain-object": ["is-plain-object@2.0.4", "", { "dependencies": { "isobject": "^3.0.1" } }, "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og=="], - - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - - "is-unicode-supported": ["is-unicode-supported@0.1.0", "", {}, "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw=="], - - "is-wsl": ["is-wsl@1.1.0", "", {}, "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw=="], - - "isarray": ["isarray@1.0.0", "", {}, "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="], + "is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "isobject": ["isobject@3.0.1", "", {}, "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg=="], - - "jest-environment-node": ["jest-environment-node@29.7.0", "", { "dependencies": { "@jest/environment": "^29.7.0", "@jest/fake-timers": "^29.7.0", "@jest/types": "^29.6.3", "@types/node": "*", "jest-mock": "^29.7.0", "jest-util": "^29.7.0" } }, "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw=="], - "jest-get-type": ["jest-get-type@29.6.3", "", {}, "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw=="], - "jest-message-util": ["jest-message-util@29.7.0", "", { "dependencies": { "@babel/code-frame": "^7.12.13", "@jest/types": "^29.6.3", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", "pretty-format": "^29.7.0", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w=="], - - "jest-mock": ["jest-mock@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "jest-util": "^29.7.0" } }, "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw=="], - "jest-util": ["jest-util@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", "graceful-fs": "^4.2.9", "picomatch": "^2.2.3" } }, "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA=="], "jest-validate": ["jest-validate@29.7.0", "", { "dependencies": { "@jest/types": "^29.6.3", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.6.3", "leven": "^3.1.0", "pretty-format": "^29.7.0" } }, "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw=="], "jest-worker": ["jest-worker@29.7.0", "", { "dependencies": { "@types/node": "*", "jest-util": "^29.7.0", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw=="], - "joi": ["joi@17.13.3", "", { "dependencies": { "@hapi/hoek": "^9.3.0", "@hapi/topo": "^5.1.0", "@sideway/address": "^4.1.5", "@sideway/formula": "^3.0.1", "@sideway/pinpoint": "^2.0.0" } }, "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA=="], - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], - - "jsc-android": ["jsc-android@250231.0.0", "", {}, "sha512-rS46PvsjYmdmuz1OAWXY/1kCYG7pnf1TBqeTiOJr1iDz7s5DLxxC9n/ZMknLDxzYzNVfI7R95MH10emSSG1Wuw=="], - "jsc-safe-url": ["jsc-safe-url@0.2.4", "", {}, "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q=="], - "jscodeshift": ["jscodeshift@0.14.0", "", { "dependencies": { "@babel/core": "^7.13.16", "@babel/parser": "^7.13.16", "@babel/plugin-proposal-class-properties": "^7.13.0", "@babel/plugin-proposal-nullish-coalescing-operator": "^7.13.8", "@babel/plugin-proposal-optional-chaining": "^7.13.12", "@babel/plugin-transform-modules-commonjs": "^7.13.8", "@babel/preset-flow": "^7.13.13", "@babel/preset-typescript": "^7.13.0", "@babel/register": "^7.13.16", "babel-core": "^7.0.0-bridge.0", "chalk": "^4.1.2", "flow-parser": "0.*", "graceful-fs": "^4.2.4", "micromatch": "^4.0.4", "neo-async": "^2.5.0", "node-dir": "^0.1.17", "recast": "^0.21.0", "temp": "^0.8.4", "write-file-atomic": "^2.3.0" }, "peerDependencies": { "@babel/preset-env": "^7.1.6" }, "bin": { "jscodeshift": "bin/jscodeshift.js" } }, "sha512-7eCC1knD7bLUPuSCwXsMZUH51O8jIcoVyKtI6P0XM0IVzlGjckPy3FIwQlorzbN0Sg79oK+RlohN32Mqf/lrYA=="], - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - "json-parse-better-errors": ["json-parse-better-errors@1.0.2", "", {}, "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw=="], - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "jsonfile": ["jsonfile@4.0.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg=="], - - "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], - - "kleur": ["kleur@3.0.3", "", {}, "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w=="], - "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], "lighthouse-logger": ["lighthouse-logger@1.4.2", "", { "dependencies": { "debug": "^2.6.9", "marky": "^1.2.2" } }, "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g=="], - "locate-path": ["locate-path@5.0.0", "", { "dependencies": { "p-locate": "^4.1.0" } }, "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g=="], - - "lodash.debounce": ["lodash.debounce@4.0.8", "", {}, "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow=="], - "lodash.throttle": ["lodash.throttle@4.1.1", "", {}, "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ=="], - "log-symbols": ["log-symbols@4.1.0", "", { "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" } }, "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg=="], - - "logkitty": ["logkitty@0.7.1", "", { "dependencies": { "ansi-fragments": "^0.2.1", "dayjs": "^1.8.15", "yargs": "^15.1.0" }, "bin": { "logkitty": "bin/logkitty.js" } }, "sha512-/3ER20CTTbahrCrpYfPn7Xavv9diBROZpoXGVZDWMw4b/X4uuUwAC0ki85tgsdMRONURyIJbcOvS94QsUBYPbQ=="], - "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "make-dir": ["make-dir@2.1.0", "", { "dependencies": { "pify": "^4.0.1", "semver": "^5.6.0" } }, "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA=="], - "makeerror": ["makeerror@1.0.12", "", { "dependencies": { "tmpl": "1.0.5" } }, "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg=="], "marky": ["marky@1.3.0", "", {}, "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ=="], @@ -761,141 +347,83 @@ "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "metro": ["metro@0.80.12", "", { "dependencies": { "@babel/code-frame": "^7.0.0", "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/template": "^7.0.0", "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "accepts": "^1.3.7", "chalk": "^4.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^2.2.0", "denodeify": "^1.2.1", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.23.1", "image-size": "^1.0.2", "invariant": "^2.2.4", "jest-worker": "^29.6.3", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.80.12", "metro-cache": "0.80.12", "metro-cache-key": "0.80.12", "metro-config": "0.80.12", "metro-core": "0.80.12", "metro-file-map": "0.80.12", "metro-resolver": "0.80.12", "metro-runtime": "0.80.12", "metro-source-map": "0.80.12", "metro-symbolicate": "0.80.12", "metro-transform-plugins": "0.80.12", "metro-transform-worker": "0.80.12", "mime-types": "^2.1.27", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "strip-ansi": "^6.0.0", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-1UsH5FzJd9quUsD1qY+zUG4JY3jo3YEMxbMYH9jT6NK3j4iORhlwTK8fYTfAUBhDKjgLfKjAh7aoazNE23oIRA=="], + "metro": ["metro@0.84.6", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "accepts": "^2.0.0", "ci-info": "^2.0.0", "connect": "^3.6.5", "debug": "^4.4.0", "error-stack-parser": "^2.0.6", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "hermes-parser": "0.35.0", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "jsc-safe-url": "^0.2.2", "lodash.throttle": "^4.1.1", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-config": "0.84.6", "metro-core": "0.84.6", "metro-file-map": "0.84.6", "metro-resolver": "0.84.6", "metro-runtime": "0.84.6", "metro-source-map": "0.84.6", "metro-symbolicate": "0.84.6", "metro-transform-plugins": "0.84.6", "metro-transform-worker": "0.84.6", "mime-types": "^3.0.1", "nullthrows": "^1.1.1", "serialize-error": "^2.1.0", "source-map": "^0.5.6", "throat": "^5.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "bin": { "metro": "src/cli.js" } }, "sha512-ty/tx/imE5Ph2gvPU788Ckim6RAC2tyZxLz2hWRgC9U7/fGKV7qY/slADy0qHPEw9cpUgc4BfmV3oUsYwm7oeg=="], - "metro-babel-transformer": ["metro-babel-transformer@0.80.12", "", { "dependencies": { "@babel/core": "^7.20.0", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.23.1", "nullthrows": "^1.1.1" } }, "sha512-YZziRs0MgA3pzCkkvOoQRXjIoVjvrpi/yRlJnObyIvMP6lFdtyG4nUGIwGY9VXnBvxmXD6mPY2e+NSw6JAyiRg=="], + "metro-babel-transformer": ["metro-babel-transformer@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "flow-enums-runtime": "^0.0.6", "hermes-parser": "0.35.0", "metro-cache-key": "0.84.6", "nullthrows": "^1.1.1" } }, "sha512-B1ozl6KxFHbQjXZ2U8WiTPWXylqiD3V4EXyV5r0fqETOrqIrJlBvbVTD82UxIN+B3Vpe4l8G1tb15ocd2RyOQA=="], - "metro-cache": ["metro-cache@0.80.12", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "metro-core": "0.80.12" } }, "sha512-p5kNHh2KJ0pbQI/H7ZBPCEwkyNcSz7OUkslzsiIWBMPQGFJ/xArMwkV7I+GJcWh+b4m6zbLxE5fk6fqbVK1xGA=="], + "metro-cache": ["metro-cache@0.84.6", "", { "dependencies": { "exponential-backoff": "^3.1.1", "flow-enums-runtime": "^0.0.6", "https-proxy-agent": "^7.0.5", "metro-core": "0.84.6" } }, "sha512-KBJVpb02oKNO+jlWQ1Aax7cd11YP0MClDaMgx6C4MhyZ9g95J/dC1Bo6sCzXaU9L9h/w5cQfT5zDkGW5sOj99A=="], - "metro-cache-key": ["metro-cache-key@0.80.12", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-o4BspKnugg/pE45ei0LGHVuBJXwRgruW7oSFAeSZvBKA/sGr0UhOGY3uycOgWInnS3v5yTTfiBA9lHlNRhsvGA=="], + "metro-cache-key": ["metro-cache-key@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-6tyXt1BZ/3U183XV48oif7Nm65TE+pTo0Vs31u4FxMgqiYQR/SiSr3RoXn3pmy4pd280ZmLmct0x3jZpu0pV4A=="], - "metro-config": ["metro-config@0.80.12", "", { "dependencies": { "connect": "^3.6.5", "cosmiconfig": "^5.0.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.6.3", "metro": "0.80.12", "metro-cache": "0.80.12", "metro-core": "0.80.12", "metro-runtime": "0.80.12" } }, "sha512-4rwOWwrhm62LjB12ytiuR5NgK1ZBNr24/He8mqCsC+HXZ+ATbrewLNztzbAZHtFsrxP4D4GLTGgh96pCpYLSAQ=="], + "metro-config": ["metro-config@0.84.6", "", { "dependencies": { "connect": "^3.6.5", "flow-enums-runtime": "^0.0.6", "jest-validate": "^29.7.0", "metro": "0.84.6", "metro-cache": "0.84.6", "metro-core": "0.84.6", "metro-runtime": "0.84.6", "yaml": "^2.6.1" } }, "sha512-cD2mLEofcuV26kxSV4hfV1Pbuh6igdUhYQriO8ZVeJ7HyZfq4/wqHV0gSR7fEO8NUp9odr60/Kzm5YakmghWwQ=="], - "metro-core": ["metro-core@0.80.12", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.80.12" } }, "sha512-QqdJ/yAK+IpPs2HU/h5v2pKEdANBagSsc6DRSjnwSyJsCoHlmyJKCaCJ7KhWGx+N4OHxh37hoA8fc2CuZbx0Fw=="], + "metro-core": ["metro-core@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "lodash.throttle": "^4.1.1", "metro-resolver": "0.84.6" } }, "sha512-ZqYskM8+f3PFMosBDJd8DLqRvT1ltRGhWL/ZfTpBvf4SoadO30VVrdhAoHFVGz3G1CJpWa3fgZH7nnnX/ybllA=="], - "metro-file-map": ["metro-file-map@0.80.12", "", { "dependencies": { "anymatch": "^3.0.3", "debug": "^2.2.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.6.3", "micromatch": "^4.0.4", "node-abort-controller": "^3.1.1", "nullthrows": "^1.1.1", "walker": "^1.0.7" }, "optionalDependencies": { "fsevents": "^2.3.2" } }, "sha512-sYdemWSlk66bWzW2wp79kcPMzwuG32x1ZF3otI0QZTmrnTaaTiGyhE66P1z6KR4n2Eu5QXiABa6EWbAQv0r8bw=="], + "metro-file-map": ["metro-file-map@0.84.6", "", { "dependencies": { "debug": "^4.4.0", "fb-watchman": "^2.0.0", "flow-enums-runtime": "^0.0.6", "graceful-fs": "^4.2.4", "invariant": "^2.2.4", "jest-worker": "^29.7.0", "micromatch": "^4.0.4", "nullthrows": "^1.1.1", "walker": "^1.0.7" } }, "sha512-ov9VywWBsHPt54Zc7/DxWvqjTwxuZvhfeyHg8ZAMGKtf2PcH1wS3EnoBH7In53Rspk/eXVWAO5+avxunPn081w=="], - "metro-minify-terser": ["metro-minify-terser@0.80.12", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-muWzUw3y5k+9083ZoX9VaJLWEV2Jcgi+Oan0Mmb/fBNMPqP9xVDuy4pOMn/HOiGndgfh/MK7s4bsjkyLJKMnXQ=="], + "metro-minify-terser": ["metro-minify-terser@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "terser": "^5.15.0" } }, "sha512-SqhB/Kxrw0XfyW0k8mEscqX/CQopm6Fp3EpdSIPzvKfGgQp9bTMahg9PAdVsXUmIWwErbmFw1VqB+VFBOKksUw=="], - "metro-resolver": ["metro-resolver@0.80.12", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-PR24gYRZnYHM3xT9pg6BdbrGbM/Cu1TcyIFBVlAk7qDAuHkUNQ1nMzWumWs+kwSvtd9eZGzHoucGJpTUEeLZAw=="], + "metro-resolver": ["metro-resolver@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-PplpGc/OCLxgKeLNsLQO/VHsYDIvkcGGr3zNT7UanHCGRqhxKv245o+naV8cRI4/pcLqyc+9j+J0AJbT3Kp5Sg=="], - "metro-runtime": ["metro-runtime@0.80.12", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-LIx7+92p5rpI0i6iB4S4GBvvLxStNt6fF0oPMaUd1Weku7jZdfkCZzmrtDD9CSQ6EPb0T9NUZoyXIxlBa3wOCw=="], + "metro-runtime": ["metro-runtime@0.84.6", "", { "dependencies": { "@babel/runtime": "^7.25.0", "flow-enums-runtime": "^0.0.6" } }, "sha512-47EYO/DOai0PA3GoAyDpyXOgHNMwbl/Z9dQlQineDLHkpD42xOwMrIx1crT0+n6F+aGoEyxZmeFwfMzaIqVqKQ=="], - "metro-source-map": ["metro-source-map@0.80.12", "", { "dependencies": { "@babel/traverse": "^7.20.0", "@babel/types": "^7.20.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.80.12", "nullthrows": "^1.1.1", "ob1": "0.80.12", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-o+AXmE7hpvM8r8MKsx7TI21/eerYYy2DCDkWfoBkv+jNkl61khvDHlQn0cXZa6lrcNZiZkl9oHSMcwLLIrFmpw=="], + "metro-source-map": ["metro-source-map@0.84.6", "", { "dependencies": { "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-symbolicate": "0.84.6", "nullthrows": "^1.1.1", "ob1": "0.84.6", "source-map": "^0.5.6", "vlq": "^1.0.0" } }, "sha512-4p/EplRbgNxhdqA6kAj1AmIq9qqYXYSAjgxuZ73rYA+p037xDDrxK8CyDKNfbn1k6aMuVt8LV0fpt7NHUTKiZw=="], - "metro-symbolicate": ["metro-symbolicate@0.80.12", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.80.12", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "through2": "^2.0.1", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-/dIpNdHksXkGHZXARZpL7doUzHqSNxgQ8+kQGxwpJuHnDhGkENxB5PS2QBaTDdEcmyTMjS53CN1rl9n1gR6fmw=="], + "metro-symbolicate": ["metro-symbolicate@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "metro-source-map": "0.84.6", "nullthrows": "^1.1.1", "source-map": "^0.5.6", "vlq": "^1.0.0" }, "bin": { "metro-symbolicate": "src/index.js" } }, "sha512-wJFyyF5ysbVyoYVWGL6GwVMiNnGllwDU2gF096376fl0fc8IFUlADSyNlswj31K9og2sOilXe/FdyWFPczPUYg=="], - "metro-transform-plugins": ["metro-transform-plugins@0.80.12", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/template": "^7.0.0", "@babel/traverse": "^7.20.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-WQWp00AcZvXuQdbjQbx1LzFR31IInlkCDYJNRs6gtEtAyhwpMMlL2KcHmdY+wjDO9RPcliZ+Xl1riOuBecVlPA=="], + "metro-transform-plugins": ["metro-transform-plugins@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "flow-enums-runtime": "^0.0.6", "nullthrows": "^1.1.1" } }, "sha512-pROPMFaj25Y9+3LlLqpBRz/7B/2YPnfEQz/z3juYxkh3FGQ+3c/Qh4E+khPSTdwTw0M6LitLYHMqaHMbQmuBJw=="], - "metro-transform-worker": ["metro-transform-worker@0.80.12", "", { "dependencies": { "@babel/core": "^7.20.0", "@babel/generator": "^7.20.0", "@babel/parser": "^7.20.0", "@babel/types": "^7.20.0", "flow-enums-runtime": "^0.0.6", "metro": "0.80.12", "metro-babel-transformer": "0.80.12", "metro-cache": "0.80.12", "metro-cache-key": "0.80.12", "metro-minify-terser": "0.80.12", "metro-source-map": "0.80.12", "metro-transform-plugins": "0.80.12", "nullthrows": "^1.1.1" } }, "sha512-KAPFN1y3eVqEbKLx1I8WOarHPqDMUa8WelWxaJCNKO/yHCP26zELeqTJvhsQup+8uwB6EYi/sp0b6TGoh6lOEA=="], + "metro-transform-worker": ["metro-transform-worker@0.84.6", "", { "dependencies": { "@babel/core": "^7.25.2", "@babel/generator": "^7.29.1", "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "flow-enums-runtime": "^0.0.6", "metro": "0.84.6", "metro-babel-transformer": "0.84.6", "metro-cache": "0.84.6", "metro-cache-key": "0.84.6", "metro-minify-terser": "0.84.6", "metro-source-map": "0.84.6", "metro-transform-plugins": "0.84.6", "nullthrows": "^1.1.1" } }, "sha512-xLTVrOENaHR9DiuhnHS+zcle0rKsCy2CpXSIx7xD+zhJs+qVRNUyV7Z1zIt/0P/d0cSGVx846R+dIjHEudhcww=="], "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["mime@2.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg=="], - - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + "mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], - "mkdirp": ["mkdirp@0.5.6", "", { "dependencies": { "minimist": "^1.2.6" }, "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw=="], + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - - "negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="], - - "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - - "nocache": ["nocache@3.0.4", "", {}, "sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw=="], - - "node-abort-controller": ["node-abort-controller@3.1.1", "", {}, "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ=="], - - "node-dir": ["node-dir@0.1.17", "", { "dependencies": { "minimatch": "^3.0.2" } }, "sha512-tmPX422rYgofd4epzrNoOXiE8XFZYOcCq1vD7MAXCDO+O+zndlA2ztdKKMa+EeuBG5tHETpr4ml4RGgpqDCCAg=="], - - "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], - "node-stream-zip": ["node-stream-zip@1.15.0", "", {}, "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="], - - "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - - "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], - "ob1": ["ob1@0.80.12", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-VMArClVT6LkhUGpnuEoBuyjG9rzUyEzg4PDkav6wK1cLhOK02gPCYFxoiB4mqVnrMhDpIzJcrGNAMVi9P+hXrw=="], + "ob1": ["ob1@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-+s+6zjd0X68hfAcn92NsEPXnSYOF3O7exw/Fj4UfRTB+UL+Tdjdkzu5/4WquXlzqgvNm8FWuuW6/0Yd8S2rYxw=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - - "on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="], + "on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - - "open": ["open@6.4.0", "", { "dependencies": { "is-wsl": "^1.1.0" } }, "sha512-IFenVPgF70fSm1keSd2iDBIDIBZkroLeuffXq+wKTzTJlBpesFWojV9lb8mzOfaAzM1sr7HQHuO0vtV0zYekGg=="], - - "ora": ["ora@5.4.1", "", { "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", "cli-cursor": "^3.1.0", "cli-spinners": "^2.5.0", "is-interactive": "^1.0.0", "is-unicode-supported": "^0.1.0", "log-symbols": "^4.1.0", "strip-ansi": "^6.0.0", "wcwidth": "^1.0.1" } }, "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ=="], - - "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], - - "p-locate": ["p-locate@4.1.0", "", { "dependencies": { "p-limit": "^2.2.0" } }, "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A=="], - - "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], - - "parse-json": ["parse-json@4.0.0", "", { "dependencies": { "error-ex": "^1.3.1", "json-parse-better-errors": "^1.0.1" } }, "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw=="], + "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], - "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - - "path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "pify": ["pify@4.0.1", "", {}, "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g=="], - - "pirates": ["pirates@4.0.7", "", {}, "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA=="], - - "pkg-dir": ["pkg-dir@3.0.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw=="], - - "pretty-format": ["pretty-format@26.6.2", "", { "dependencies": { "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", "react-is": "^17.0.1" } }, "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg=="], - - "process-nextick-args": ["process-nextick-args@2.0.1", "", {}, "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="], + "pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], "promise": ["promise@8.3.0", "", { "dependencies": { "asap": "~2.0.6" } }, "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg=="], - "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], - - "prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="], - - "queue": ["queue@6.0.2", "", { "dependencies": { "inherits": "~2.0.3" } }, "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA=="], - "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], "react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], - "react-devtools-core": ["react-devtools-core@4.28.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-cq/o30z9W2Wb4rzBefjv5fBalHU0rJGZCHAkf/RHSBWSSYwh8PlQTqqOJmgIIbBtpj27T6FIPXeomIjZtCNVqA=="], + "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], - "react-native": ["react-native@0.73.11", "", { "dependencies": { "@jest/create-cache-key-function": "^29.6.3", "@react-native-community/cli": "12.3.7", "@react-native-community/cli-platform-android": "12.3.7", "@react-native-community/cli-platform-ios": "12.3.7", "@react-native/assets-registry": "0.73.1", "@react-native/codegen": "0.73.3", "@react-native/community-cli-plugin": "0.73.18", "@react-native/gradle-plugin": "0.73.5", "@react-native/js-polyfills": "0.73.1", "@react-native/normalize-colors": "0.73.2", "@react-native/virtualized-lists": "0.73.4", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "base64-js": "^1.5.1", "chalk": "^4.0.0", "deprecated-react-native-prop-types": "^5.0.0", "event-target-shim": "^5.0.1", "flow-enums-runtime": "^0.0.6", "invariant": "^2.2.4", "jest-environment-node": "^29.6.3", "jsc-android": "^250231.0.0", "memoize-one": "^5.0.0", "metro-runtime": "^0.80.3", "metro-source-map": "^0.80.3", "mkdirp": "^0.5.1", "nullthrows": "^1.1.1", "pretty-format": "^26.5.2", "promise": "^8.3.0", "react-devtools-core": "^4.27.7", "react-refresh": "^0.14.0", "react-shallow-renderer": "^16.15.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.24.0-canary-efb381bbf-20230505", "stacktrace-parser": "^0.1.10", "whatwg-fetch": "^3.0.0", "ws": "^6.2.2", "yargs": "^17.6.2" }, "peerDependencies": { "react": "18.2.0" }, "bin": { "react-native": "cli.js" } }, "sha512-yvQIX+ZXOHMFnhmwZ1fBpRI/53k+iLN8DxVf24Fx4ABU63RGAYfyCZC0/3W+5OUVx4KSIZUv4Tv+/NGIieBOwg=="], + "react-native": ["react-native@0.85.2", "", { "dependencies": { "@react-native/assets-registry": "0.85.2", "@react-native/codegen": "0.85.2", "@react-native/community-cli-plugin": "0.85.2", "@react-native/gradle-plugin": "0.85.2", "@react-native/js-polyfills": "0.85.2", "@react-native/normalize-colors": "0.85.2", "@react-native/virtualized-lists": "0.85.2", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-plugin-syntax-hermes-parser": "0.33.3", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "hermes-compiler": "250829098.0.10", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.0", "metro-source-map": "^0.84.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@react-native/jest-preset": "0.85.2", "@types/react": "^19.1.1", "react": "^19.2.3" }, "optionalPeers": ["@react-native/jest-preset", "@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-GFWEPwLYirfj5X8gMtXOWtqX0cqUEURRHETZfFk37VCa4++izrKvGvv24anvuyulXV87NAhVkfNw93rLg3HByw=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], @@ -903,41 +431,11 @@ "react-test-renderer": ["react-test-renderer@18.2.0", "", { "dependencies": { "react-is": "^18.2.0", "react-shallow-renderer": "^16.15.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA=="], - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "readline": ["readline@1.3.0", "", {}, "sha512-k2d6ACCkiNYz222Fs/iNze30rRJ1iIicW7JuX/7/cozvih6YCkFZH+J6mAFDVgv0dRBaAyr4jDqC95R2y4IADg=="], - - "recast": ["recast@0.21.5", "", { "dependencies": { "ast-types": "0.15.2", "esprima": "~4.0.0", "source-map": "~0.6.1", "tslib": "^2.0.1" } }, "sha512-hjMmLaUXAm1hIuTqOdeYObMslq/q+Xff6QE3Y2P+uoHAg2nmVlLBps2hzh1UJDdMtDTMXOFewK6ky51JQIeECg=="], - - "regenerate": ["regenerate@1.4.2", "", {}, "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A=="], - - "regenerate-unicode-properties": ["regenerate-unicode-properties@10.2.0", "", { "dependencies": { "regenerate": "^1.4.2" } }, "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA=="], - "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], - "regenerator-transform": ["regenerator-transform@0.15.2", "", { "dependencies": { "@babel/runtime": "^7.8.4" } }, "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg=="], - - "regexpu-core": ["regexpu-core@6.2.0", "", { "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" } }, "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA=="], - - "regjsgen": ["regjsgen@0.8.0", "", {}, "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q=="], - - "regjsparser": ["regjsparser@0.12.0", "", { "dependencies": { "jsesc": "~3.0.2" }, "bin": { "regjsparser": "bin/parser" } }, "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-main-filename": ["require-main-filename@2.0.0", "", {}, "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg=="], - - "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="], - - "resolve-from": ["resolve-from@3.0.0", "", {}, "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw=="], - - "restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], - - "rimraf": ["rimraf@2.6.3", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "./bin.js" } }, "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA=="], - - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - - "scheduler": ["scheduler@0.24.0-canary-efb381bbf-20230505", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-ABvovCDe/k9IluqSh4/ISoq8tIJnW8euVAWYt5j/bg6dRnqwQwiGO1F/V4AyK96NGF/FB04FhOUDuWj8IKfABA=="], + "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], @@ -947,65 +445,35 @@ "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], - "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], - "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "shallow-clone": ["shallow-clone@3.0.1", "", { "dependencies": { "kind-of": "^6.0.2" } }, "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], "shell-quote": ["shell-quote@1.8.2", "", {}, "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], - - "sisteransi": ["sisteransi@1.0.5", "", {}, "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="], - - "slash": ["slash@3.0.0", "", {}, "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q=="], - - "slice-ansi": ["slice-ansi@2.1.0", "", { "dependencies": { "ansi-styles": "^3.2.0", "astral-regex": "^1.0.0", "is-fullwidth-code-point": "^2.0.0" } }, "sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ=="], - "source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], "source-map-support": ["source-map-support@0.5.21", "", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="], - "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], - - "stack-utils": ["stack-utils@2.0.6", "", { "dependencies": { "escape-string-regexp": "^2.0.0" } }, "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ=="], - "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], "stacktrace-parser": ["stacktrace-parser@0.1.11", "", { "dependencies": { "type-fest": "^0.7.1" } }, "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg=="], - "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + "statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "string_decoder": ["string_decoder@1.1.1", "", { "dependencies": { "safe-buffer": "~5.1.0" } }, "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg=="], - - "strip-ansi": ["strip-ansi@5.2.0", "", { "dependencies": { "ansi-regex": "^4.1.0" } }, "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA=="], + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], - - "strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="], - - "sudo-prompt": ["sudo-prompt@9.2.1", "", {}, "sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw=="], - - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], - - "temp": ["temp@0.8.4", "", { "dependencies": { "rimraf": "~2.6.2" } }, "sha512-s0ZZzd0BzYv5tLSptZooSjK8oj6C+c19p7Vqta9+6NPOf7r+fxq0cJe6/oN4LTC79sy5NY8ucOJNgwsKCSbfqg=="], - - "temp-dir": ["temp-dir@2.0.0", "", {}, "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg=="], + "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], "terser": ["terser@5.39.0", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw=="], "throat": ["throat@5.0.0", "", {}, "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA=="], - "through2": ["through2@2.0.5", "", { "dependencies": { "readable-stream": "~2.3.6", "xtend": "~4.0.1" } }, "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tmpl": ["tmpl@1.0.5", "", {}, "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw=="], @@ -1013,63 +481,29 @@ "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], - "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], - - "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - - "type-detect": ["type-detect@4.0.8", "", {}, "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g=="], - "type-fest": ["type-fest@0.7.1", "", {}, "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg=="], "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "unicode-canonical-property-names-ecmascript": ["unicode-canonical-property-names-ecmascript@2.0.1", "", {}, "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg=="], - - "unicode-match-property-ecmascript": ["unicode-match-property-ecmascript@2.0.0", "", { "dependencies": { "unicode-canonical-property-names-ecmascript": "^2.0.0", "unicode-property-aliases-ecmascript": "^2.0.0" } }, "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q=="], - - "unicode-match-property-value-ecmascript": ["unicode-match-property-value-ecmascript@2.2.0", "", {}, "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg=="], - - "unicode-property-aliases-ecmascript": ["unicode-property-aliases-ecmascript@2.1.0", "", {}, "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w=="], - - "universalify": ["universalify@0.1.2", "", {}, "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg=="], - "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], "update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], - "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vlq": ["vlq@1.0.1", "", {}, "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w=="], "walker": ["walker@1.0.8", "", { "dependencies": { "makeerror": "1.0.12" } }, "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], - - "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "whatwg-fetch": ["whatwg-fetch@3.6.20", "", {}, "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg=="], - "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - - "write-file-atomic": ["write-file-atomic@2.4.3", "", { "dependencies": { "graceful-fs": "^4.1.11", "imurmurhash": "^0.1.4", "signal-exit": "^3.0.2" } }, "sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ=="], - - "ws": ["ws@6.2.3", "", { "dependencies": { "async-limiter": "~1.0.0" } }, "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA=="], - - "xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="], + "ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -1081,206 +515,168 @@ "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "@babel/code-frame/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], - "@babel/core/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - - "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/core/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], - "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/core/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], - "@babel/helper-create-class-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/core/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], - "@babel/helper-create-regexp-features-plugin/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/helper-define-polyfill-provider/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/generator/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], - "@babel/plugin-transform-runtime/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/generator/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], - "@babel/preset-env/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + "@babel/helper-module-imports/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], - "@babel/traverse/debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + "@babel/helper-module-imports/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], - "@jest/environment/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], + "@babel/helper-module-transforms/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], - "@jest/fake-timers/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], + "@babel/helper-module-transforms/@babel/traverse": ["@babel/traverse@7.27.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "@babel/generator": "^7.27.0", "@babel/parser": "^7.27.0", "@babel/template": "^7.27.0", "@babel/types": "^7.27.0", "debug": "^4.3.1", "globals": "^11.1.0" } }, "sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA=="], - "@jest/types/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], + "@babel/helpers/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], - "@react-native-community/cli-server-api/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "@babel/runtime/regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], - "@react-native-community/cli-tools/find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + "@babel/template/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], - "@react-native/dev-middleware/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], + "@babel/template/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], - "accepts/negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + "@babel/traverse/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "babel-plugin-polyfill-corejs2/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@babel/traverse/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - "chrome-launcher/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], + "@babel/traverse/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "chrome-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@jest/types/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], - "chromium-edge-launcher/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], + "chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "chromium-edge-launcher/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - "chromium-edge-launcher/mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="], + "chrome-launcher/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], - "chromium-edge-launcher/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], + "connect/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "finalhandler/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "finalhandler/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "finalhandler/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], - - "finalhandler/statuses": ["statuses@1.5.0", "", {}, "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA=="], - - "hermes-profile-transformer/source-map": ["source-map@0.7.4", "", {}, "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA=="], - - "jest-environment-node/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], - - "jest-message-util/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], - - "jest-mock/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], + "http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "jest-util/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], "jest-util/ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="], - "jest-validate/pretty-format": ["pretty-format@29.7.0", "", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="], + "jest-util/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "jest-worker/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - - "logkitty/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], + "lighthouse-logger/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "make-dir/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], + "metro/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "metro/hermes-parser": ["hermes-parser@0.23.1", "", { "dependencies": { "hermes-estree": "0.23.1" } }, "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA=="], + "metro/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - "metro/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "metro/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "metro/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "metro/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], - "metro-babel-transformer/hermes-parser": ["hermes-parser@0.23.1", "", { "dependencies": { "hermes-estree": "0.23.1" } }, "sha512-oxl5h2DkFW83hT4DAUJorpah8ou4yvmweUzLJmmr6YV2cezduCdlil1AvU/a/xSsAFo4WUcNA4GoV5Bvq6JffA=="], + "metro-babel-transformer/hermes-parser": ["hermes-parser@0.35.0", "", { "dependencies": { "hermes-estree": "0.35.0" } }, "sha512-9JLjeHxBx8T4CAsydZR49PNZUaix+WpQJwu9p2010lu+7Kwl6D/7wYFFJxoz+aXkaaClp9Zfg6W6/zVlSJORaA=="], - "ora/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "metro-transform-plugins/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - "pkg-dir/find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + "metro-transform-plugins/@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="], - "pretty-format/@jest/types": ["@jest/types@26.6.2", "", { "dependencies": { "@types/istanbul-lib-coverage": "^2.0.0", "@types/istanbul-reports": "^3.0.0", "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" } }, "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ=="], + "metro-transform-worker/@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="], - "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], - - "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], - - "react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], "react-test-renderer/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], - "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - - "regjsparser/jsesc": ["jsesc@3.0.2", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g=="], + "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], - "send/mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - - "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - - "slice-ansi/ansi-styles": ["ansi-styles@3.2.1", "", { "dependencies": { "color-convert": "^1.9.0" } }, "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA=="], + "send/on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], - "slice-ansi/is-fullwidth-code-point": ["is-fullwidth-code-point@2.0.0", "", {}, "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w=="], + "send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="], - - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "string_decoder/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], - - "strip-ansi/ansi-regex": ["ansi-regex@4.1.1", "", {}, "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g=="], - "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "through2/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - - "wrap-ansi/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - "@babel/core/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@babel/core/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], - "@babel/helper-define-polyfill-provider/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@babel/core/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], - "@babel/traverse/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "@babel/generator/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], - "@jest/environment/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@babel/generator/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], - "@jest/fake-timers/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@babel/helper-module-imports/@babel/traverse/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], - "@jest/types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "@react-native-community/cli-tools/find-up/locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + "@babel/helper-module-imports/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], - "@react-native/dev-middleware/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], + "@babel/helper-module-imports/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], - "chrome-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/parser": ["@babel/parser@7.27.0", "", { "dependencies": { "@babel/types": "^7.27.0" }, "bin": "./bin/babel-parser.js" }, "sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg=="], - "chromium-edge-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/types": ["@babel/types@7.27.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.25.9", "@babel/helper-validator-identifier": "^7.25.9" } }, "sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg=="], - "jest-environment-node/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@babel/helpers/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], - "jest-message-util/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "@babel/helpers/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], - "jest-mock/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - - "jest-util/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@babel/template/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], - "jest-validate/pretty-format/ansi-styles": ["ansi-styles@5.2.0", "", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + "@babel/template/@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.25.9", "", {}, "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ=="], - "jest-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@babel/traverse/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "logkitty/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], + "@babel/traverse/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "logkitty/yargs/y18n": ["y18n@4.0.3", "", {}, "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ=="], + "@jest/types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "logkitty/yargs/yargs-parser": ["yargs-parser@18.1.3", "", { "dependencies": { "camelcase": "^5.0.0", "decamelize": "^1.2.0" } }, "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ=="], + "chrome-launcher/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.23.1", "", {}, "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg=="], + "connect/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "metro/hermes-parser/hermes-estree": ["hermes-estree@0.23.1", "", {}, "sha512-eT5MU3f5aVhTqsfIReZ6n41X5sYn4IdQL0nvz6yO+MMlPxw49aSARHLg/MSehQftyjnrE8X6bYregzSumqc6cg=="], + "finalhandler/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "pkg-dir/find-up/locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + "jest-util/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "pretty-format/@jest/types/@types/node": ["@types/node@22.15.2", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A=="], + "jest-worker/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "pretty-format/@jest/types/@types/yargs": ["@types/yargs@15.0.19", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-2XUaGVmyQjgyAZldf0D0c14vvo/yv0MhQBSTJcejMMaitsn3nxCB6TmH4G0ZQf+uxROOa9mpanoSm8h6SG/1ZA=="], + "lighthouse-logger/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "slice-ansi/ansi-styles/color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + "metro-babel-transformer/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], - "through2/readable-stream/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + "metro-transform-plugins/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "@react-native-community/cli-tools/find-up/locate-path/p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "metro-transform-plugins/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "logkitty/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "metro-transform-plugins/@babel/template/@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="], - "logkitty/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + "metro-transform-worker/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "logkitty/yargs/yargs-parser/camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], + "metro-transform-worker/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "pkg-dir/find-up/locate-path/p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + "metro/@babel/generator/@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], - "pkg-dir/find-up/locate-path/path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "metro/@babel/generator/@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], - "pretty-format/@jest/types/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "metro/hermes-parser/hermes-estree": ["hermes-estree@0.35.0", "", {}, "sha512-xVx5Opwy8Oo1I5yGpVRhCvWL/iV3M+ylksSKVNlxxD90cpDpR/AR1jLYqK8HWihm065a6UI3HeyAmYzwS8NOOg=="], - "slice-ansi/ansi-styles/color-convert/color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + "send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], - "@react-native-community/cli-tools/find-up/locate-path/p-locate/p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + "@babel/helper-module-transforms/@babel/traverse/@babel/types/@babel/helper-string-parser": ["@babel/helper-string-parser@7.25.9", "", {}, "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA=="], } } diff --git a/package.json b/package.json index ef08020a..281e2b43 100644 --- a/package.json +++ b/package.json @@ -94,13 +94,13 @@ }, "devDependencies": { "@biomejs/biome": "^2.5.5", - "@react-native/typescript-config": "0.84.1", + "@react-native/typescript-config": "0.85.2", "@types/bun": "^1.3.11", "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-test-renderer": "18.0.7", "react": "18.2.0", - "react-native": "0.73", + "react-native": "0.85.2", "react-test-renderer": "18.2.0", "typescript": "^7.0.2" } From 7c215607848ecf891b4410a65f121009002f6942 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:24:38 +0800 Subject: [PATCH 11/21] chore(harmony): mirror NATIVE_CONFIG_CONFLICT from error_codes.h Co-Authored-By: Claude Opus 5.5 --- harmony/pushy/src/main/ets/ErrorCodes.ts | 7 ++----- harmony/pushy/src/main/ets/PushyTurboModule.ts | 4 ++-- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/harmony/pushy/src/main/ets/ErrorCodes.ts b/harmony/pushy/src/main/ets/ErrorCodes.ts index 7e36cf74..bb989e08 100644 --- a/harmony/pushy/src/main/ets/ErrorCodes.ts +++ b/harmony/pushy/src/main/ets/ErrorCodes.ts @@ -22,11 +22,8 @@ export const ERROR_UNSUPPORTED_PLATFORM = 'UNSUPPORTED_PLATFORM'; export const ERROR_APK_INSTALL_PERMISSION_REQUIRED = 'APK_INSTALL_PERMISSION_REQUIRED'; export const ERROR_APK_INSTALL_FAILED = 'APK_INSTALL_FAILED'; -// C2:JS syncNativeConfig 撞上宿主原生 configure()。暂作本地常量(不带 -// ERROR_ 前缀,check-constant-parity 只核对 ERROR_* 与 error_codes.h): -// kNativeConfigConflict 由另一侧加入 cpp/patch_core/error_codes.h,合并时 -// 改名为 ERROR_NATIVE_CONFIG_CONFLICT 纳入镜像核对。 -export const NATIVE_CONFIG_CONFLICT_CODE = 'NATIVE_CONFIG_CONFLICT'; +// JS syncNativeConfig 撞上宿主原生 configure()。 +export const ERROR_NATIVE_CONFIG_CONFLICT = 'NATIVE_CONFIG_CONFLICT'; /** * 带稳定错误码的 Error:`code` 是属性,消息同时带 `[CODE] ` 前缀(桥丢属性时 diff --git a/harmony/pushy/src/main/ets/PushyTurboModule.ts b/harmony/pushy/src/main/ets/PushyTurboModule.ts index b236329c..c17f10a6 100644 --- a/harmony/pushy/src/main/ets/PushyTurboModule.ts +++ b/harmony/pushy/src/main/ets/PushyTurboModule.ts @@ -22,7 +22,7 @@ import { ERROR_RESTART_FAILED, ERROR_SWITCH_VERSION_FAILED, ERROR_UNSUPPORTED_PLATFORM, - NATIVE_CONFIG_CONFLICT_CODE, + ERROR_NATIVE_CONFIG_CONFLICT, createPushyError, getErrorMessage, toPushyError, @@ -270,7 +270,7 @@ export class PushyTurboModule extends UITurboModule { logger.debug(TAG, ',call syncNativeConfig'); if (this.context.isHostConfigured()) { throw createPushyError( - NATIVE_CONFIG_CONFLICT_CODE, + ERROR_NATIVE_CONFIG_CONFLICT, "host configured natively; set nativeConfigSource: 'native' in JS", ); } From 6393eda42ea4dd90491e311d48f71386bf5e5cdf Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:25:00 +0800 Subject: [PATCH 12/21] docs: move the design documents into docs/ NATIVE_CHECKUPDATE_DESIGN, NATIVE_CHECK_FOLLOWUPS, BUNDLEHASH_MIGRATION and NATIVE_HOST_API_MIGRATION leave the repository root. The README links and the two path-style mentions (check-native-spec-parity.js, harmony-build.yml) follow; code comments cite them by name only and need no change. check-packlist.js now forbids docs/ outright instead of matching the old root file names. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/harmony-build.yml | 2 +- CODE_AUDIT.md | 2 +- README-CN.md | 2 +- README.md | 2 +- BUNDLEHASH_MIGRATION.md => docs/BUNDLEHASH_MIGRATION.md | 0 .../NATIVE_CHECKUPDATE_DESIGN.md | 0 NATIVE_CHECK_FOLLOWUPS.md => docs/NATIVE_CHECK_FOLLOWUPS.md | 0 .../NATIVE_HOST_API_MIGRATION.md | 0 scripts/check-native-spec-parity.js | 2 +- scripts/check-packlist.js | 3 ++- 10 files changed, 7 insertions(+), 6 deletions(-) rename BUNDLEHASH_MIGRATION.md => docs/BUNDLEHASH_MIGRATION.md (100%) rename NATIVE_CHECKUPDATE_DESIGN.md => docs/NATIVE_CHECKUPDATE_DESIGN.md (100%) rename NATIVE_CHECK_FOLLOWUPS.md => docs/NATIVE_CHECK_FOLLOWUPS.md (100%) rename NATIVE_HOST_API_MIGRATION.md => docs/NATIVE_HOST_API_MIGRATION.md (100%) diff --git a/.github/workflows/harmony-build.yml b/.github/workflows/harmony-build.yml index aa7ee4e0..bc166c13 100644 --- a/.github/workflows/harmony-build.yml +++ b/.github/workflows/harmony-build.yml @@ -3,7 +3,7 @@ name: harmony-build # Compiles the HarmonyOS HAR on pull requests that touch its inputs, in the # same pinned container publish.yml uses. Until this job existed the HAR was # only compiled at release time, so an ArkTS type error, a broken NAPI export -# or a stale C++ method table (NATIVE_CHECK_FOLLOWUPS.md, 2026-08-13) was +# or a stale C++ method table (docs/NATIVE_CHECK_FOLLOWUPS.md, 2026-08-13) was # first seen when publishing. assembleHar runs the ArkTS compiler over the # .ets/.ts sources, which is the real type check; check-harmony-types.js # adds a strict tsc pass over the .ts sources and must not skip here. diff --git a/CODE_AUDIT.md b/CODE_AUDIT.md index 58c1ead0..b2823a8b 100644 --- a/CODE_AUDIT.md +++ b/CODE_AUDIT.md @@ -290,7 +290,7 @@ NATIVE_CHECK_FOLLOWUPS 中两项已在代码里修复、可关闭:"iOS 合流 - **6.3(P3)根 package.json 里的死依赖与死脚本**:`firebase-tools`、`jest`、`ts-jest`、`@types/jest`、`detox`、`pod-install` 在根目录无任何使用(detox/pod-install 由 `Example/e2etest` 自己的 lock 提供;测试跑的是 `bun test`);`tests:emulator:prepare / tests:emulator:start-ci / tests:packager:jet-ci / tests:ios:pod:install` 脚本和整个 `.github/workflows/scripts/`(firebase emulator、functions、rules)是模板残留,仓库里没有任何 workflow 引用;根 `e2e/starter.test.js` 测的是不存在的 welcome/hello 元素。清掉后 `bun install` 少装 ~30MB,lockfile 与 dependabot 面都小。 - **6.4(P3)仓库里的大二进制**:`Example/harmony_use_pushy/debug.png`(761KB)、`demo.png`(345KB)在 git 历史里;`android/lib/*/librnupdate.so` 每次重建都是 4×~290KB 的新 blob(历史里已有 3 代)。发布已改为 CI 重建(publish.yml),可以考虑不再提交 `.so`(本地开发走 `bun build:so`,或 git LFS)。 - **6.5(P3)README 与公开 API**:`testChannel / healthCheck / autoMarkSuccessDelayMs / resetToPackagedBundle / getUpdateMetadata / attachToSentry / maxRetries / beforeReload / onPackageExpired / overridePackageVersion / dismissErrorAfter / useUpdateProgress / onError` 在两份 README 里均无提及(只有外站文档链接)。至少在 README 里放一张 options 索引表指向文档页。 -- **6.6(P4)设计文档**:`NATIVE_CHECKUPDATE_DESIGN.md`(36KB)、`NATIVE_CHECK_FOLLOWUPS.md`(31KB)、`BUNDLEHASH_MIGRATION.md`(25KB)都在仓库根;FOLLOWUPS 里大量已关闭项与原始描述并存。建议移到 `docs/`,FOLLOWUPS 只保留开放项表。 +- **6.6(P4)设计文档**:`NATIVE_CHECKUPDATE_DESIGN.md`(36KB)、`NATIVE_CHECK_FOLLOWUPS.md`(31KB)、`BUNDLEHASH_MIGRATION.md`(25KB)都在仓库根;FOLLOWUPS 里大量已关闭项与原始描述并存。建议移到 `docs/`,FOLLOWUPS 只保留开放项表。(2026-10-12:四份设计文档已移到 `docs/`;FOLLOWUPS 精简未做。) - **6.7(P4)方法表对等性**:本次核对 16 个 Spec 方法在 oldarch/newarch/iOS/鸿蒙 C++/鸿蒙 ArkTS 五处全部注册(好)。但没有自动化守护,鸿蒙已经断过一次;见 5.2 的脚本建议。另 iOS 未导出 Spec 声明的 `isUsingBundleUrl` 常量(Android/鸿蒙有;JS 侧无人使用)——要么 iOS 补上,要么从 Spec 删掉。 - **6.8(P3)安全模型说明**:三端均无 bundle 签名校验(grep 无 signature/ed25519/publicKey),完整性完全依赖 TLS + 服务端下发的 hash;端点列表又可由 gitee/jsdelivr 上的 `endpoints.json` 远程注入(`getRemoteEndpoints` 只要求是字符串,不要求 https)。这是既定信任模型,但两处低成本加固值得做:远程端点列表只接受 `https://`;长期看可加可选的离线签名(app 内置公钥、服务端签 manifest),对自托管/http 部署尤其有价值。 diff --git a/README-CN.md b/README-CN.md index 3cf212cb..a6d93eff 100644 --- a/README-CN.md +++ b/README-CN.md @@ -178,4 +178,4 @@ Hermes 字节码对通用二进制 diff 极不友好,我们在两个环节同 原生宿主入口统一为 `prepareBundle`:Android 使用 `PushyRuntime`, Apple 平台使用 `RCTPushy`,Harmony 使用已有的 `PushyFileJSBundleProvider`。 请在正常启动 bundle 解析后调用。改名不改变 JS 桥接契约,但原生接入代码需要迁移并重新构建。 -详见[原生接口迁移说明](NATIVE_HOST_API_MIGRATION.md)。 +详见[原生接口迁移说明](docs/NATIVE_HOST_API_MIGRATION.md)。 diff --git a/README.md b/README.md index 219cda21..5f408404 100644 --- a/README.md +++ b/README.md @@ -160,5 +160,5 @@ Since 10.52.1 every cold start runs one background update check that **does not 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 +[native host API migration guide](docs/NATIVE_HOST_API_MIGRATION.md) for the source-breaking rename, unchanged JS bridge contract, and native rebuild steps. diff --git a/BUNDLEHASH_MIGRATION.md b/docs/BUNDLEHASH_MIGRATION.md similarity index 100% rename from BUNDLEHASH_MIGRATION.md rename to docs/BUNDLEHASH_MIGRATION.md diff --git a/NATIVE_CHECKUPDATE_DESIGN.md b/docs/NATIVE_CHECKUPDATE_DESIGN.md similarity index 100% rename from NATIVE_CHECKUPDATE_DESIGN.md rename to docs/NATIVE_CHECKUPDATE_DESIGN.md diff --git a/NATIVE_CHECK_FOLLOWUPS.md b/docs/NATIVE_CHECK_FOLLOWUPS.md similarity index 100% rename from NATIVE_CHECK_FOLLOWUPS.md rename to docs/NATIVE_CHECK_FOLLOWUPS.md diff --git a/NATIVE_HOST_API_MIGRATION.md b/docs/NATIVE_HOST_API_MIGRATION.md similarity index 100% rename from NATIVE_HOST_API_MIGRATION.md rename to docs/NATIVE_HOST_API_MIGRATION.md diff --git a/scripts/check-native-spec-parity.js b/scripts/check-native-spec-parity.js index 5202a7b7..1bf769b0 100644 --- a/scripts/check-native-spec-parity.js +++ b/scripts/check-native-spec-parity.js @@ -8,7 +8,7 @@ // A method missing on one platform is `undefined` on the JS side, and the JS // feature-detects then treat the whole capability as "old native" and skip it // silently — exactly how the native cold-start check shipped dark on Harmony -// for three releases (NATIVE_CHECK_FOLLOWUPS.md, 2026-08-13). +// for three releases (docs/NATIVE_CHECK_FOLLOWUPS.md, 2026-08-13). const fs = require('fs'); const path = require('path'); diff --git a/scripts/check-packlist.js b/scripts/check-packlist.js index d78924d5..de32a01b 100644 --- a/scripts/check-packlist.js +++ b/scripts/check-packlist.js @@ -37,7 +37,8 @@ const forbidden = [ /^\.github\//, /^\.gitmodules$/, /^biome\.json$/, - /^(BUNDLEHASH|NATIVE_CHECK|CODE_AUDIT)[A-Z_]*\.md$/, + /^docs\//, + /^CODE_AUDIT\.md$/, /\.(log|tgz|zip|ppk|apk)$/, ]; From e1c6fbe3d14f96e9c0a706fef01854f2df263b50 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:26:17 +0800 Subject: [PATCH 13/21] test: import URL from node:url for RN 0.85 type declarations Co-Authored-By: Claude Opus 5.5 --- src/__tests__/jniRegistration.test.ts | 1 + src/__tests__/nativeConfiguration.test.ts | 1 + src/__tests__/nativeHostApi.test.ts | 1 + src/__tests__/nativeHostApiNaming.test.ts | 1 + src/__tests__/nativeTextEncoding.test.ts | 1 + 5 files changed, 5 insertions(+) diff --git a/src/__tests__/jniRegistration.test.ts b/src/__tests__/jniRegistration.test.ts index dd81fb82..7835eaab 100644 --- a/src/__tests__/jniRegistration.test.ts +++ b/src/__tests__/jniRegistration.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'bun:test'; import { readdirSync, readFileSync } from 'node:fs'; +import { URL } from 'node:url'; import { decodeNativeText } from '../../scripts/encode-native-text'; // JNI_OnLoad binds Android native methods from an encoded name/signature table diff --git a/src/__tests__/nativeConfiguration.test.ts b/src/__tests__/nativeConfiguration.test.ts index 4da9cded..1c548cd3 100644 --- a/src/__tests__/nativeConfiguration.test.ts +++ b/src/__tests__/nativeConfiguration.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; +import { URL } from 'node:url'; import { runInNewContext } from 'node:vm'; import type { PushyConfiguration } from '../../harmony/pushy/src/main/ets/PushyConfiguration'; import { normalizePushyConfiguration } from '../../harmony/pushy/src/main/ets/PushyConfiguration'; diff --git a/src/__tests__/nativeHostApi.test.ts b/src/__tests__/nativeHostApi.test.ts index 2b9eb771..5c6f3b34 100644 --- a/src/__tests__/nativeHostApi.test.ts +++ b/src/__tests__/nativeHostApi.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'bun:test'; import { readFileSync } from 'node:fs'; +import { URL } from 'node:url'; import { runInNewContext } from 'node:vm'; import type { BundlePreparationResult } from '../../harmony/pushy/src/main/ets/BundlePreparationResult'; import { diff --git a/src/__tests__/nativeHostApiNaming.test.ts b/src/__tests__/nativeHostApiNaming.test.ts index e29aba68..0439a351 100644 --- a/src/__tests__/nativeHostApiNaming.test.ts +++ b/src/__tests__/nativeHostApiNaming.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'bun:test'; import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { URL } from 'node:url'; const root = new URL('../../', import.meta.url); const android = 'android/src/main/java/cn/reactnative/modules/update/'; diff --git a/src/__tests__/nativeTextEncoding.test.ts b/src/__tests__/nativeTextEncoding.test.ts index 682e76c8..ebc04ba4 100644 --- a/src/__tests__/nativeTextEncoding.test.ts +++ b/src/__tests__/nativeTextEncoding.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'bun:test'; import { readdirSync, readFileSync } from 'node:fs'; +import { URL } from 'node:url'; import { normalizePushyConfiguration } from '../../harmony/pushy/src/main/ets/PushyConfiguration'; import { QUERY_PATH, revealText } from '../../harmony/pushy/src/main/ets/Texts'; import { From c54c2b8be7b1e8a4d76d025b10e066a7c456179e Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:28:55 +0800 Subject: [PATCH 14/21] refactor(ios): move the native round into RCTPushyOrchestrator.mm Pure move, no behavior change. RCTPushyOrchestrator (its statics, the crash-hold handler and the HTTP/config helpers) now lives in its own file; the helpers both files need (state lock, defaults, state transitions, key constants, PushyHasCompletedVersionAtPath, bundle hash) switch from static to PUSHY_HIDDEN and are declared in the private RCTPushyInternal.h, together with PushyType, the private RCTPushy interface and the orchestrator interface. The podspec keeps that header out of the public umbrella. The purge-restore harness extracts from both sources (one definition across them) and accepts PUSHY_HIDDEN definitions like static ones. Co-Authored-By: Claude Opus 5.5 --- ios/RCTPushy/RCTPushy.mm | 1365 +---------------- ios/RCTPushy/RCTPushyInternal.h | 117 ++ ios/RCTPushy/RCTPushyOrchestrator.mm | 1258 +++++++++++++++ react-native-update.podspec | 8 +- scripts/test-ios-purge-restore.sh | 4 +- scripts/tests/ios-purge-restore/README.md | 9 +- scripts/tests/ios-purge-restore/extract.py | 60 +- .../ios-purge-restore/purge_restore_test.mm | 3 + 8 files changed, 1457 insertions(+), 1367 deletions(-) create mode 100644 ios/RCTPushy/RCTPushyInternal.h create mode 100644 ios/RCTPushy/RCTPushyOrchestrator.mm diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index daa00382..41408dac 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -1,4 +1,4 @@ -#import "RCTPushy.h" +#import "RCTPushyInternal.h" #import "RCTPushyConfiguration.h" #import "RCTPushyDownloader.h" #import "ZipArchive.h" @@ -31,25 +31,14 @@ #import #import -// RN's log macros pass __FILE__, which would embed this file's absolute build -// path in the binary; log with the bare file name instead. -#if RCTLOG_ENABLED -#undef _RCTLog -#define _RCTLog(lvl, ...) _RCTLogNativeInternal(lvl, __FILE_NAME__, __LINE__, __VA_ARGS__) -#endif #import #import #include #include -#if TARGET_OS_TV -#import -#include -#include -#endif // Immutable host-facing snapshot; activated always means NEXT launch. -static NSDictionary *PushyHostResult(NSString *status, NSString *reason, +PUSHY_HIDDEN NSDictionary *PushyHostResult(NSString *status, NSString *reason, NSString *hash, BOOL activated) { return @{@"status": status, @"reason": reason ?: @"", @"hash": hash ?: @"", @"activated": @(activated)}; @@ -71,13 +60,13 @@ static NSString *const paramCurrentVersion = @"currentVersion"; static NSString *const paramIsFirstTime = @"isFirstTime"; static NSString *const paramIsFirstLoadOk = @"isFirstLoadOK"; -static NSString *const keyUuid = @"REACTNATIVECN_PUSHY_UUID"; +PUSHY_HIDDEN NSString *const keyUuid = @"REACTNATIVECN_PUSHY_UUID"; static NSString *const keyHashInfo = @"REACTNATIVECN_PUSHY_HASH_"; static NSString *const keyFirstLoadMarked = @"REACTNATIVECN_PUSHY_FIRSTLOADMARKED_KEY"; static NSString *const keyRolledBackMarked = @"REACTNATIVECN_PUSHY_ROLLEDBACKMARKED_KEY"; // Encoded constants (RCTPushyRevealText) are decoded on first use, not by // static initializers at image load. -static NSString *PushyRevealOnce(NSString *__strong *slot, dispatch_once_t *once, +PUSHY_HIDDEN NSString *PushyRevealOnce(NSString *__strong *slot, dispatch_once_t *once, const char *hex) { dispatch_once(once, ^{ *slot = RCTPushyRevealText(hex); @@ -96,14 +85,14 @@ static NSString *const keyBundleHashCache = @"REACTNATIVECN_PUSHY_BUNDLEHASH_KEY"; // Raw JSON persisted by JS (syncNativeConfig) for the native cold-start // update check; parsed on read by the orchestrator. Absent = check disabled. -static NSString *const keyNativeConfig = @"REACTNATIVECN_PUSHY_NATIVE_CONFIG_KEY"; +PUSHY_HIDDEN NSString *const keyNativeConfig = @"REACTNATIVECN_PUSHY_NATIVE_CONFIG_KEY"; // Raw response cache written by the native cold-start check for the JS side // to reuse (§10.3), scoped to the request and config that produced it. -static NSString *const keyNativeCheckCache = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_RESP_KEY"; +PUSHY_HIDDEN NSString *const keyNativeCheckCache = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_RESP_KEY"; // Set when a native check round starts, cleared when it ends (§11.4). Residue // on the next launch means the previous process died mid-round (a crash // rescue was truncated): that launch resumes immediately instead of waiting. -static NSString *const keyNativeCheckIncomplete = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_INCOMPLETE_KEY"; +PUSHY_HIDDEN NSString *const keyNativeCheckIncomplete = @"REACTNATIVECN_PUSHY_NATIVE_CHECK_INCOMPLETE_KEY"; static NSString *const PushyErrorDomain = @"cn.reactnative.pushy"; // file def @@ -116,7 +105,7 @@ } // Protocol strings and version-info flags read by JS, stored encoded // (RCTPushyRevealText) so they do not appear as plain binary strings. -static NSString *PushyOptionUrlKey(void) { +PUSHY_HIDDEN NSString *PushyOptionUrlKey(void) { static NSString *value; static dispatch_once_t once; return PushyRevealOnce(&value, &once, "2f07f0d0ba8e5d572e"); @@ -131,21 +120,6 @@ static dispatch_once_t once; return PushyRevealOnce(&value, &once, "7407e4dae09b69512137"); } -static NSString *PushyInfoCrashHold(void) { - static NSString *value; - static dispatch_once_t once; - return PushyRevealOnce(&value, &once, "3905f5c2a6b96d56212a19"); -} -static NSString *PushyInfoForceBoot(void) { - static NSString *value; - static dispatch_once_t once; - return PushyRevealOnce(&value, &once, "3c18e6d2aba9674a360d19ead5a695"); -} -static NSString *PushyStatusNone(void) { - static NSString *value; - static dispatch_once_t once; - return PushyRevealOnce(&value, &once, "3418c1c1aa8a7c40"); -} #define VERSION_COMPLETE_FILE_NAME_LITERAL ".pushy-complete" static NSString * const VERSION_COMPLETE_FILE_NAME = @VERSION_COMPLETE_FILE_NAME_LITERAL; @@ -159,26 +133,16 @@ return [NSString stringWithUTF8String:code]; } -// cInfo.os label prefix; must match the JS side (core.ts) so the server -// buckets tvOS devices apart from iOS ones. -#if TARGET_OS_TV -static NSString * const PushyOsName = @"tvos"; -#else -static NSString * const PushyOsName = @"ios"; -#endif - // event def static NSString * const EVENT_PROGRESS_DOWNLOAD = @"RCTPushyDownloadProgress"; static NSString * const PARAM_PROGRESS_HASH = @"hash"; static NSString * const PARAM_PROGRESS_RECEIVED = @"received"; static NSString * const PARAM_PROGRESS_TOTAL = @"total"; -static NSTimeInterval PushyMonotonicNow(void) { +PUSHY_HIDDEN NSTimeInterval PushyMonotonicNow(void) { return [NSProcessInfo processInfo].systemUptime; } -static std::string PushyToStdString(NSString *value); - static NSString *PushyStagingDirForVersionDir(NSString *versionDir) { return [versionDir stringByAppendingString:@(pushy::install_record::kStagingSuffix)]; } @@ -212,7 +176,7 @@ static BOOL PushyInstallRecordMatches(NSDictionary *record, NSString *hash) { // Presence check (launch / dedup paths, no digest work): bundle present and // the record exists and, unless legacy-empty, names this version. -static BOOL PushyHasCompletedVersionAtPath(NSString *versionDir, NSString *hash) { +PUSHY_HIDDEN BOOL PushyHasCompletedVersionAtPath(NSString *versionDir, NSString *hash) { NSString *bundlePath = [versionDir stringByAppendingPathComponent:BUNDLE_FILE_NAME]; if (![[NSFileManager defaultManager] fileExistsAtPath:bundlePath]) { return NO; @@ -406,12 +370,6 @@ - (void)zipArchiveDidUnzipFileAtIndex:(NSInteger)fileIndex } -typedef NS_ENUM(NSInteger, PushyType) { - PushyTypeFullDownload = 1, - PushyTypePatchFromPackage = 2, - PushyTypePatchFromPpk = 3, -}; - static std::atomic ignoreRollback{false}; // Set once the host calls +configure:completion: in this process; JS // syncNativeConfig then rejects with NATIVE_CONFIG_CONFLICT. Local literal @@ -428,7 +386,7 @@ typedef NS_ENUM(NSInteger, PushyType) { // samples this counter and abandons activation (and its response cache) when // the value moved, so an in-flight rescue can never resurrect the version the // app just reset away from. -static std::atomic pushyResetGeneration{0}; +PUSHY_HIDDEN std::atomic pushyResetGeneration{0}; // The version whose bundle this process actually loaded (resolved in // +bundleURL). resetToPackagedBundle must not delete its directory: update // assets (images/fonts) are read from it on demand at runtime, so wiping it @@ -563,7 +521,7 @@ static void PushyFinishDownload(NSString *hash, NSError *error) { // overwritten by a concurrent bundleURL and the version being rolled back. static os_unfair_lock pushyStateLock = OS_UNFAIR_LOCK_INIT; -static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) { +PUSHY_HIDDEN void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) { #if DEBUG // Not re-entrant: a nested acquisition deadlocks in release, so catch it // where it is written. @@ -577,7 +535,7 @@ static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) { } } -static std::string PushyToStdString(NSString *value) { +PUSHY_HIDDEN std::string PushyToStdString(NSString *value) { if (value == nil) { return std::string(); } @@ -593,11 +551,11 @@ static void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)) { }]; } -static NSUserDefaults *PushyDefaults(void) { +PUSHY_HIDDEN NSUserDefaults *PushyDefaults(void) { return [NSUserDefaults standardUserDefaults]; } -static NSString *PushyFromStdString(const std::string &value) { +PUSHY_HIDDEN NSString *PushyFromStdString(const std::string &value) { if (value.empty()) { return nil; } @@ -612,7 +570,7 @@ static void PushySetNullableString(NSUserDefaults *defaults, NSString *key, NSSt } } -static NSString *PushyHashInfoKey(NSString *hash) { +PUSHY_HIDDEN NSString *PushyHashInfoKey(NSString *hash) { return [keyHashInfo stringByAppendingString:hash ?: @""]; } @@ -630,7 +588,7 @@ static BOOL PushyStringIsBlank(NSString *value) { // Server-provided identifiers (hash/originHash) are used as child names under // the download root; anything that could resolve outside of it (path // separators, "..", ".") must be rejected before touching the filesystem. -static BOOL PushyIsSafePathComponent(NSString *value) { +PUSHY_HIDDEN BOOL PushyIsSafePathComponent(NSString *value) { if (PushyStringIsBlank(value)) { return NO; } @@ -740,7 +698,7 @@ static BOOL PushyPatchManifestFromJson(NSDictionary *json, return [defaults stringForKey:key] ?: [defaults stringForKey:legacyKey]; } -static pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults) { +PUSHY_HIDDEN pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults) { pushy::state::State state; state.package_version = PushyToStdString( PushyBinaryIdentityValue(defaults, paramPackageVersion, legacyParamPackageVersion)); @@ -792,7 +750,7 @@ static void PushyApplyStateToDefaults(NSUserDefaults *defaults, const pushy::sta // hold it. Lets the cold-start check commit its whole result (version info, // switch, response cache) inside one lock acquisition, so resetToPackagedBundle // can never interleave between the generation check and the writes. -static void PushySwitchVersionLocked(NSString *hash) { +PUSHY_HIDDEN void PushySwitchVersionLocked(NSString *hash) { NSUserDefaults *defaults = PushyDefaults(); pushy::state::State next = pushy::state::SwitchVersion( PushyStateFromDefaults(defaults), @@ -803,243 +761,7 @@ static void PushySwitchVersionLocked(NSString *hash) { ignoreRollback = false; } -@interface RCTPushy () -- (void)fetchPackage:(PushyType)type - options:(NSDictionary *)options - resolver:(RCTPromiseResolveBlock)resolve - rejecter:(RCTPromiseRejectBlock)reject; -- (void)performFetch:(PushyType)type - options:(NSDictionary *)options - callback:(void (^)(NSError *error))callback; -- (NSError *)restartBridgeWithReason:(NSString *)reason; -- (void)unzipDownloadedPackage:(NSString *)zipFilePath - hash:(NSString *)hash - type:(PushyType)type - originHash:(NSString *)originHash - callback:(void (^)(NSError *error))callback; -- (void)finishDownloadedPackage:(NSString *)hash - type:(PushyType)type - originHash:(NSString *)originHash - callback:(void (^)(NSError *error))callback; -- (void)applyDeltaForHash:(NSString *)hash - type:(PushyType)type - fromBundle:(NSString *)bundleOrigin - source:(NSString *)sourceOrigin - callback:(void (^)(NSError *error))callback; -- (BOOL)switchVersion:(NSString *)hash error:(NSError **)error; -- (BOOL)ensureDirectoryExistsAtPath:(NSString *)path; -+ (void)excludeFromBackup:(NSString *)path; -- (void)unzipFileAtPath:(NSString *)path - toDestination:(NSString *)destination - completionHandler:(void (^)(NSError *error))completionHandler; -+ (NSString *)downloadDir; -+ (NSURL *)binaryBundleURL; -+ (NSString *)packageVersion; -+ (NSString *)buildTime; -@end -// Native cold-start update check (NATIVE_CHECKUPDATE_DESIGN §10): runs once -// per process, a few seconds after launch, entirely independent of the app -// bundle — this is what lets a bricked hot update be replaced on the next -// launch. Decisions come from cpp/update_flow_core; this class is IO glue. -@interface RCTPushyOrchestrator : NSObject -+ (void)persistConfiguration:(NSString *)config; -+ (BOOL)hasRunnableConfig; -+ (NSDictionary *)prepareBundle; -+ (NSDictionary *)prepareBundleWithin:(NSTimeInterval)timeout; -+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion; -#if TARGET_OS_TV && !DEBUG -+ (BOOL)restorePurgedLaunch:(NSString *)purgedVersion - rolledBack:(NSString *)launchRolledBackVersion; -#endif -+ (void)recordJsRound:(NSString *)config; -+ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime; -+ (BOOL)claimRound; -+ (void)runClaimedRoundWithDeadline:(NSTimeInterval)deadlineUptime; -+ (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime; -+ (void)runHoldRoundWithDeadline:(NSTimeInterval)deadlineUptime; -+ (BOOL)commitRoundWithGeneration:(uint64_t)generation - configGeneration:(uint64_t)configGeneration - hashInfo:(NSDictionary *)hashInfoEntry - activate:(NSString *)hashToActivate - responseText:(NSString *)responseText - request:(NSString *)requestBody - config:(NSString *)configJson - responseAt:(long long)responseAtSeconds - activated:(BOOL *)activatedOut; -@end - -// One round per process, whoever starts it first — the delayed cold-start -// path or the crash-rescue thread (§11.3). The semaphore lets the rescue -// wait out an in-flight round instead of racing it. -static std::atomic pushyRoundStarted{false}; -static std::atomic pushyRoundCompleted{false}; -// Flipped the moment a crash is being held. JS is dead from that point on, -// so there is no second decision maker: the round force-activates whatever -// it downloads (§11.3). -static std::atomic pushyCrashHoldActive{false}; -static std::atomic pushyHoldAttempted{false}; -// Set when this process's round is the tvOS purged-version restore. -static std::atomic pushyPurgeRestoreActive{false}; -// True while +bundleURL waits for that restore. Only then may the round -// activate what it downloads on its own (JS has not started to decide), since -// the launch resolves the bundle again afterwards. Closed once the wait ends, -// timeout or not; guarded by the state lock that every activation commits -// under, so a commit lands either before the close (and launches) or after it -// (and is left to JS) — never selected behind the packaged bundle's back. -static bool pushyPurgeRestoreWindowOpen = false; -static dispatch_semaphore_t pushyRoundDone; -static NSString *pushyLaunchRolledBackForHold = nil; -// A version this process downloaded but left for JS to activate. If the -// process then crashes, JS will never activate it — the crash handler -// activates it directly (bounded local work, no network). The generation is -// the one the round committed under: a reset that lands afterwards bumps it, -// and the late activation must lose to that reset exactly like the round -// itself would. Guarded by @synchronized (RCTPushyOrchestrator class). -static NSString *pushyUnactivatedHash = nil; -static uint64_t pushyUnactivatedGeneration = 0; -static uint64_t pushyUnactivatedConfigGeneration = 0; -static NSUncaughtExceptionHandler *pushyPreviousExceptionHandler = NULL; -// ≈ process start: scheduleFromColdStart runs during the first bundleURL. -static NSTimeInterval pushyProcessAnchorUptime = 0; -// Config JSON for which JS reported a completed check in this process -// (markJsCheckCompleted). Process-scoped by design. Guarded by -// @synchronized (RCTPushyOrchestrator class). -static NSString *pushyJsCompletedConfig = nil; -// The group supplements (rather than consumes) the crash-rescue semaphore. -static dispatch_group_t pushyHostRoundGroup; -static std::atomic pushySyncReady{false}; -static NSDictionary *pushyHostRoundResult = nil; -static NSString *pushyHostRoundConfig = nil; -static uint64_t pushyHostRoundGeneration = 0; -// Bumped by persistConfiguration when the config really changes (C1 -// equivalence fails); resets keep pushyResetGeneration to themselves. -static std::atomic pushyNativeConfigGeneration{0}; -static uint64_t pushyHostRoundConfigGeneration = 0; - -static const NSTimeInterval kPushyHoldTriggerUptime = 60; -static const NSTimeInterval kPushyHoldBudgetBackgroundThread = 10; -// A held main thread freezes UI teardown; stay well under the watchdog. -static const NSTimeInterval kPushyHoldBudgetMainThread = 3.5; -// The purged-version restore blocks +bundleURL, usually inside -// application:didFinishLaunching: — stay clear of the launch watchdog. The -// budget is measured from process start; with less than the minimum left the -// restore is not attempted. -#if TARGET_OS_TV && !DEBUG -static const NSTimeInterval kPushyPurgeRestoreBudget = 12; -static const NSTimeInterval kPushyPurgeRestoreMinimumBudget = 2; - -// Seconds since the kernel started this process; 0 when unknown. -static NSTimeInterval PushyProcessElapsed(void) { - struct kinfo_proc info; - size_t size = sizeof(info); - int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; - if (sysctl(mib, 4, &info, &size, NULL, 0) != 0 || size == 0) { - return 0; - } - struct timeval now; - gettimeofday(&now, NULL); - const struct timeval start = info.kp_proc.p_starttime; - NSTimeInterval elapsed = (now.tv_sec - start.tv_sec) - + (now.tv_usec - start.tv_usec) / 1e6; - return elapsed > 0 ? elapsed : 0; -} - -// NO only when the system positively reports no usable network path; an -// unknown state (no answer within the short wait) lets the restore try. -static BOOL PushyHasNetworkPath(void) { - dispatch_queue_t queue = dispatch_queue_create("cn.reactnative.pushy.path", DISPATCH_QUEUE_SERIAL); - nw_path_monitor_t monitor = nw_path_monitor_create(); - dispatch_semaphore_t answered = dispatch_semaphore_create(0); - __block nw_path_status_t status = nw_path_status_invalid; - nw_path_monitor_set_queue(monitor, queue); - nw_path_monitor_set_update_handler(monitor, ^(nw_path_t path) { - BOOL first = status == nw_path_status_invalid; - status = nw_path_get_status(path); - if (first) { - dispatch_semaphore_signal(answered); - } - }); - nw_path_monitor_start(monitor); - dispatch_semaphore_wait(answered, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 2)); - nw_path_monitor_cancel(monitor); - __block nw_path_status_t result; - dispatch_sync(queue, ^{ - result = status; - }); - return result != nw_path_status_unsatisfied; -} -#endif -// prepareBundleWithCompletion: (no explicit timeout) waits this long at most: -// past the longest round (8 query requests, then two 600s download phases). -static const NSTimeInterval kPushyHostPrepareDefaultTimeout = 1800; - -static void PushyMaybeHoldProcess(void) { - // Once per process; a second crashing thread passes straight through - // instead of waiting behind the first (§11.3: prefer under-rescuing over - // wedging the teardown). - bool expected = false; - if (!pushyHoldAttempted.compare_exchange_strong(expected, true)) { - return; - } - NSTimeInterval uptime = PushyMonotonicNow() - pushyProcessAnchorUptime; - bool roundInFlight = pushyRoundStarted.load() && !pushyRoundCompleted.load(); - // Early crashes are the brick signature; a crash with an in-flight round - // is worth finishing regardless of uptime. Everything else is an ordinary - // crash whose UX must not be delayed. - if (uptime >= kPushyHoldTriggerUptime && !roundInFlight) { - return; - } - NSTimeInterval budget = [NSThread isMainThread] - ? kPushyHoldBudgetMainThread - : kPushyHoldBudgetBackgroundThread; - NSTimeInterval deadline = PushyMonotonicNow() + budget; - NSLog(@"RCTPushy -- crash hold: holding process for up to %.1fs " - @"(uptime %.1fs)", budget, uptime); - - // The rescue runs on its own queue and the dying thread only waits with a - // hard timeout: even a deadlocked rescue (a thread that died holding a - // lock — uncaught ObjC exceptions skip @finally) can only delay the - // process death, never prevent it. - dispatch_semaphore_t done = dispatch_semaphore_create(0); - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ - @try { - [RCTPushyOrchestrator runHoldRoundWithDeadline:deadline]; - } @catch (NSException *exception) { - NSLog(@"RCTPushy -- crash hold failed: %@", exception.reason); - } - dispatch_semaphore_signal(done); - }); - if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(budget * NSEC_PER_SEC))) != 0) { - NSLog(@"RCTPushy -- crash hold: budget exhausted, letting go"); - } -} - -// Crash-moment brick rescue (§11): when the app is dying of an uncaught -// exception during startup (RCTFatal raises NSException in release), the -// process is still alive and JS will never run again — a natural, -// false-positive-free window to finish the cold-start check. The previous -// handler (crash reporters chain the same way) always runs afterwards. -static void PushyCrashHoldExceptionHandler(NSException *exception) { - @try { - PushyMaybeHoldProcess(); - } @catch (NSException *inner) { - // The dying process owes the previous handler its turn no matter - // what the rescue did. - } - if (pushyPreviousExceptionHandler != NULL) { - pushyPreviousExceptionHandler(exception); - } -} - -static void PushyInstallCrashHoldHandler(void) { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - pushyPreviousExceptionHandler = NSGetUncaughtExceptionHandler(); - NSSetUncaughtExceptionHandler(&PushyCrashHoldExceptionHandler); - }); -} // Single-flight for the digest in PushyBundleHashSync: the delayed cold-start // round (utility queue) and JS getBundleHash (_fileQueue) can both miss the @@ -1068,7 +790,7 @@ static void PushyInstallCrashHoldHandler(void) { // main thread). Without it only the cache is consulted: a crash rescue on // the main thread has a 3.5s budget in total, and hashing a large bundle on // a fresh install would spend all of it before the first request. -static NSString *PushyBundleHashSync(BOOL computeIfMissing) { +PUSHY_HIDDEN NSString *PushyBundleHashSync(BOOL computeIfMissing) { NSString *path = [[RCTPushy binaryBundleURL] path]; if (path == nil) { return @""; @@ -2433,1050 +2155,3 @@ + (NSString *)buildTime #endif @end - -#pragma mark - native cold-start check orchestration - -// Refuses an https→http downgrade before the redirected request — and with -// it the POST body (appKey, uuid, versions) — leaves the device. The shared -// session would follow first and only the response could be rejected; ATS -// blocks plaintext by default, this covers apps that opted out with -// NSAllowsArbitraryLoads. Same rule as RCTPushyDownloader. Returning nil -// delivers the 3xx itself as the final response, which the status check in -// PushyHttpRequest then treats as a failed endpoint. -@interface PushyRequestRedirectGuard : NSObject -@end - -@implementation PushyRequestRedirectGuard - -- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task -willPerformHTTPRedirection:(NSHTTPURLResponse *)response - newRequest:(NSURLRequest *)request - completionHandler:(void (^)(NSURLRequest *))completionHandler -{ - NSString *originalScheme = [task.originalRequest.URL.scheme lowercaseString]; - NSString *nextScheme = [request.URL.scheme lowercaseString]; - if ([originalScheme isEqualToString:@"https"] && [nextScheme isEqualToString:@"http"]) { - completionHandler(nil); - return; - } - completionHandler(request); -} - -@end - -// One long-lived delegate session (not the shared one) so the redirect guard -// sees the 3xx before it is followed; completion handlers still receive the -// final response. Never invalidated: it lives as long as the process. -static NSURLSession *PushyRequestSession(void) { - static NSURLSession *session; - static dispatch_once_t once; - dispatch_once(&once, ^{ - session = [NSURLSession - sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] - delegate:[PushyRequestRedirectGuard new] - delegateQueue:nil]; - }); - return session; -} - -// Same shape as -[UIDevice systemVersion] ("17.4", "17.4.1"), which is -// main-thread-only UIKit; the round runs on a background queue. -static NSString *PushyOsVersion(void) { - NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; - if (version.patchVersion > 0) { - return [NSString stringWithFormat:@"%ld.%ld.%ld", (long)version.majorVersion, - (long)version.minorVersion, (long)version.patchVersion]; - } - return [NSString stringWithFormat:@"%ld.%ld", (long)version.majorVersion, - (long)version.minorVersion]; -} - -// Blocking JSON HTTP round-trip on the orchestrator's utility thread. Returns -// the response body on 2xx, nil on any failure. The semaphore timeout is a -// backstop over the request's own timeoutInterval. -static NSString *PushyHttpRequest(NSString *urlString, NSString *method, - NSString *body, NSTimeInterval timeout) { - NSURL *url = [NSURL URLWithString:urlString]; - if (url == nil) { - return nil; - } - NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; - request.HTTPMethod = method; - request.timeoutInterval = timeout; - [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; - if (body != nil) { - [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; - request.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding]; - } - dispatch_semaphore_t sem = dispatch_semaphore_create(0); - __block NSString *result = nil; - NSURLSessionDataTask *task = [PushyRequestSession() dataTaskWithRequest:request - completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { - NSHTTPURLResponse *httpResponse = - [response isKindOfClass:[NSHTTPURLResponse class]] - ? (NSHTTPURLResponse *)response - : nil; - NSInteger status = httpResponse.statusCode; - if (error == nil && httpResponse != nil && status >= 200 && status < 300 - && data != nil) { - result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; - } - dispatch_semaphore_signal(sem); - }]; - [task resume]; - if (dispatch_semaphore_wait( - sem, dispatch_time(DISPATCH_TIME_NOW, - (int64_t)((timeout + 5) * NSEC_PER_SEC))) != 0) { - [task cancel]; - return nil; - } - return result; -} - -static NSString *PushyNormalizeEndpointBase(NSString *base) { - while ([base hasSuffix:@"/"]) { - base = [base substringToIndex:base.length - 1]; - } - return base; -} - -static BOOL PushyIsValidResponse(NSString *responseText) { - if (responseText == nil) { - return NO; - } - // Shared schema rule (update_flow_core::IsValidResponse): a 200 with - // `{"error": ...}` is a failed endpoint, not a verdict. - return flowcore::IsValidResponse(PushyToStdString(responseText)) ? YES : NO; -} - -// Canonical config equality, shared with the JS, Android and Harmony sides: -// two config JSON strings are equivalent iff their canonical forms deep-equal. -// Canonical form: the parsed object with absent keys defaulted -// (disabled:false, queryUrls:[], rnu:"", rn:"", afterDownload:"none") and the -// endpoints/queryUrls entries stripped of trailing '/' and deduplicated in -// first-occurrence order; key order is irrelevant. Unparseable is never -// equivalent. Lets a host-owned config (sorted keys) and the same config as JS -// serializes it count as one, instead of a "change" that cancels the round. -static BOOL PushyJsonIsBool(id value) { - return [value isKindOfClass:NSNumber.class] - && CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID(); -} - -static BOOL PushyJsonEqual(id a, id b) { - if (PushyJsonIsBool(a) != PushyJsonIsBool(b)) { - return NO; // false is not 0 - } - if ([a isKindOfClass:NSDictionary.class] && [b isKindOfClass:NSDictionary.class]) { - if ([a count] != [b count]) { - return NO; - } - for (id key in (NSDictionary *)a) { - id other = ((NSDictionary *)b)[key]; - if (other == nil || !PushyJsonEqual(((NSDictionary *)a)[key], other)) { - return NO; - } - } - return YES; - } - if ([a isKindOfClass:NSArray.class] && [b isKindOfClass:NSArray.class]) { - if ([a count] != [b count]) { - return NO; - } - for (NSUInteger i = 0; i < [a count]; i++) { - if (!PushyJsonEqual(((NSArray *)a)[i], ((NSArray *)b)[i])) { - return NO; - } - } - return YES; - } - return [a isEqual:b]; -} - -static id PushyCanonicalUrlList(id value) { - if (![value isKindOfClass:NSArray.class]) { - return value; - } - NSMutableArray *result = [NSMutableArray array]; - for (id item in (NSArray *)value) { - id entry = [item isKindOfClass:NSString.class] ? PushyNormalizeEndpointBase(item) : item; - BOOL seen = NO; - for (id existing in result) { - if (PushyJsonEqual(existing, entry)) { - seen = YES; - break; - } - } - if (!seen) { - [result addObject:entry]; - } - } - return result; -} - -static NSDictionary *PushyCanonicalConfig(NSString *json) { - NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; - id object = data == nil ? nil - : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; - if (![object isKindOfClass:NSDictionary.class]) { - return nil; - } - NSMutableDictionary *config = [object mutableCopy]; - NSDictionary *defaults = @{@"disabled": @NO, @"queryUrls": @[], @"rnu": @"", - @"rn": @"", @"afterDownload": @"none"}; - for (NSString *key in defaults) { - if (config[key] == nil) { - config[key] = defaults[key]; - } - } - for (NSString *key in @[@"endpoints", @"queryUrls"]) { - if (config[key] != nil) { - config[key] = PushyCanonicalUrlList(config[key]); - } - } - return config; -} - -static BOOL PushyConfigsEquivalent(NSString *a, NSString *b) { - NSDictionary *left = a == nil ? nil : PushyCanonicalConfig(a); - NSDictionary *right = b == nil ? nil : PushyCanonicalConfig(b); - return left != nil && right != nil && PushyJsonEqual(left, right); -} - -// The persisted config a round may run with. Every gate (crash-hold handler -// install, round start, prepareBundle, the round itself) reads it through -// PushyLoadRunnableConfig, so they agree on what "configured" and "disabled" -// mean. disabled follows JS truthiness, like the flow core. -struct PushyRunnableConfig { - NSString *json; - flowjson::Value value; - NSString *appKey; -}; - -// YES when a round may run; otherwise *reason is not_configured, disabled or -// invalid_config. -static BOOL PushyLoadRunnableConfig(PushyRunnableConfig *config, NSString **reason) { - NSString *failure = nil; - NSString *json = [PushyDefaults() stringForKey:keyNativeConfig]; - config->json = [json copy]; - if (json.length == 0) { - failure = @"not_configured"; - } else { - bool ok = false; - flowjson::Value value = flowjson::Parse(PushyToStdString(json), &ok); - NSString *appKey = ok && value.IsObject() - ? PushyFromStdString(value.Get("appKey").AsString()) : nil; - if (!ok || !value.IsObject()) { - failure = @"invalid_config"; - } else if (value.Get("disabled").Truthy()) { - failure = @"disabled"; - } else if (appKey.length == 0) { - failure = @"invalid_config"; - } else { - config->value = value; - config->appKey = appKey; - } - } - if (reason != NULL) { - *reason = failure; - } - return failure == nil; -} - -// Host-facing result for a config that cannot run: absent or disabled is a -// skip, a malformed one a failure. -static NSDictionary *PushyUnrunnableConfigResult(NSString *reason) { - BOOL skip = [reason isEqualToString:@"not_configured"] || [reason isEqualToString:@"disabled"]; - return PushyHostResult(skip ? @"skipped" : @"failed", reason, nil, NO); -} - -@implementation RCTPushyOrchestrator - -+ (void)persistConfiguration:(NSString *)config { - PushyWithStateLock(^{ - NSUserDefaults *defaults = PushyDefaults(); - if ([defaults stringForKey:keyUuid].length == 0) { - [defaults setObject:[NSUUID UUID].UUIDString forKey:keyUuid]; - } - if (PushyConfigsEquivalent([defaults stringForKey:keyNativeConfig], config)) { - // The same config in another serialization (key order, trailing - // slashes, defaulted keys). Not a change: the stored text, the - // response cache and any in-flight round all stay valid. - return; - } - // A different request identity/policy: rounds started under the old - // one must not commit (pushyNativeConfigGeneration, checked by - // commitRoundWithGeneration next to the reset generation), and the - // cached response and JS-completed marker belong to the old one. - pushyNativeConfigGeneration.fetch_add(1); - [defaults setObject:config forKey:keyNativeConfig]; - [defaults removeObjectForKey:keyNativeCheckCache]; - [self recordJsRound:nil]; - }); - if (pushySyncReady.load() && [self hasRunnableConfig]) { - PushyInstallCrashHoldHandler(); - } -} - -+ (BOOL)hasRunnableConfig { - PushyRunnableConfig config; - return PushyLoadRunnableConfig(&config, NULL); -} - -// Process-wide round state, set up by whichever launch path needs a round -// first: the delayed cold-start check or the tvOS purged-version restore. -+ (void)prepareProcess:(NSString *)launchRolledBackVersion { - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - pushyRoundDone = dispatch_semaphore_create(0); - pushyHostRoundGroup = dispatch_group_create(); - dispatch_group_enter(pushyHostRoundGroup); - pushyProcessAnchorUptime = PushyMonotonicNow(); - pushyLaunchRolledBackForHold = [launchRolledBackVersion copy]; - pushySyncReady.store(true); - // The crash-hold rescue shares the orchestrator's rollout gate: no - // runnable config, no handler (§11.3). persistConfiguration installs - // it once a runnable config arrives later in the process. - if ([self hasRunnableConfig]) { - PushyInstallCrashHoldHandler(); - } - }); -} - -+ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion { -#if !DEBUG - // Once per process; a few seconds of delay keeps the check away from the - // cold-start critical path (§7 R5) — its result targets the NEXT launch. - // Unless the previous process died mid-round (residual incomplete - // marker), in which case every launch second counts (§11.4). - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - [self prepareProcess:launchRolledBackVersion]; - NSUserDefaults *defaults = PushyDefaults(); - int64_t delaySeconds = - [defaults objectForKey:keyNativeCheckIncomplete] != nil ? 0 : 5; - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delaySeconds * NSEC_PER_SEC), - dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ - if ([self hasJsRound]) { - // Not consuming the round: a later crash rescue may still - // need it. - NSLog(@"RCTPushy -- native sync skipped: JS already queried in this process"); - return; - } - [self startRoundWithDeadline:0]; - }); - }); -#endif -} - -#if TARGET_OS_TV && !DEBUG -// Runs this process's round before the first bundle loads, blocking the -// caller for at most what is left of kPushyPurgeRestoreBudget since process -// start. The state no longer has a -// current version, so the server answers with the latest version for the -// packaged bundle, and the round activates it while the restore window is -// open. Returns NO when no round can run; otherwise the caller must resolve -// the launch bundle again: it launches whatever was activated before the -// window closed, or the packaged bundle. The round stops at the same -// deadline, keeping a partial download for the next check to resume. -+ (BOOL)restorePurgedLaunch:(NSString *)purgedVersion - rolledBack:(NSString *)launchRolledBackVersion { - [self prepareProcess:launchRolledBackVersion]; - if (![self hasRunnableConfig]) { - RCTLogWarn(@"RCTPushy -- version %@ was purged and no native sync is " - @"configured; launching the packaged bundle", purgedVersion); - return NO; - } - // Offline: the round could only burn the launch budget. - if (!PushyHasNetworkPath()) { - RCTLogWarn(@"RCTPushy -- version %@ was purged and there is no network; " - @"launching the packaged bundle", purgedVersion); - return NO; - } - // The launch watchdog counts from process start, not from this call: - // whatever the host did before resolving the bundle is already spent. - NSTimeInterval budget = kPushyPurgeRestoreBudget - PushyProcessElapsed(); - if (budget < kPushyPurgeRestoreMinimumBudget) { - RCTLogWarn(@"RCTPushy -- version %@ was purged and the launch budget is " - @"spent; launching the packaged bundle", purgedVersion); - return NO; - } - PushyWithStateLock(^{ - pushyPurgeRestoreWindowOpen = true; - }); - pushyPurgeRestoreActive.store(true); - NSTimeInterval deadline = PushyMonotonicNow() + budget; - dispatch_semaphore_t done = dispatch_semaphore_create(0); - dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ - [self startRoundWithDeadline:deadline]; - dispatch_semaphore_signal(done); - }); - BOOL timedOut = dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(budget * NSEC_PER_SEC))) != 0; - // The round's deadline does not bound a slow response (the request - // timeout is an idle timeout) nor a version already on disk, so the close - // is what stops a late activation — not the deadline. - PushyWithStateLock(^{ - pushyPurgeRestoreWindowOpen = false; - }); - if (timedOut) { - RCTLogWarn(@"RCTPushy -- purged version %@ restore timed out", purgedVersion); - } else { - NSDictionary *result = pushyHostRoundResult; - RCTLogInfo(@"RCTPushy -- purged version %@ restore: %@ %@ %@", purgedVersion, - result[@"status"], result[@"reason"], result[@"hash"]); - } - return YES; -} -#endif - -+ (NSDictionary *)prepareBundle { - return [self prepareBundleWithin:kPushyHostPrepareDefaultTimeout]; -} - -// Waits at most `timeout` seconds for this process's round. The round itself -// is not cut short by the host's wait (its result still serves the next -// launch); the host gets failed/timeout instead of a result. -+ (NSDictionary *)prepareBundleWithin:(NSTimeInterval)timeout { -#if DEBUG - return PushyHostResult(@"skipped", @"debug", nil, NO); -#else - if (!pushySyncReady.load()) { - return PushyHostResult(@"skipped", @"not_initialized", nil, NO); - } - PushyRunnableConfig runnable; - NSString *reason = nil; - if (!PushyLoadRunnableConfig(&runnable, &reason)) { - return PushyUnrunnableConfigResult(reason); - } - NSString *configJson = runnable.json; - // Run the round on its own queue rather than this caller's thread, so the - // wait below can be bounded. - if ([self claimRound]) { - dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ - [self runClaimedRoundWithDeadline:0]; - }); - } - // A group is broadcast-style. Sharing the rescue semaphore would let one - // waiter consume the only signal and leave the other waiting forever. - if (!pushyRoundStarted.load()) { - return PushyHostResult(@"skipped", @"config_changed", nil, NO); - } - if (!(timeout > 0)) { - timeout = 0; // also NaN - } - if (dispatch_group_wait(pushyHostRoundGroup, dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(MIN(timeout, (NSTimeInterval)(INT64_MAX / NSEC_PER_SEC)) * NSEC_PER_SEC))) != 0) { - return PushyHostResult(@"failed", @"timeout", nil, NO); - } - if (pushyHostRoundConfigGeneration != pushyNativeConfigGeneration.load() - || !PushyConfigsEquivalent(configJson, pushyHostRoundConfig) - || !PushyConfigsEquivalent(configJson, [PushyDefaults() stringForKey:keyNativeConfig])) { - return PushyHostResult(@"cancelled", @"config_changed", nil, NO); - } - if (pushyHostRoundGeneration != pushyResetGeneration.load()) { - return PushyHostResult(@"cancelled", @"reset", nil, NO); - } - return pushyHostRoundResult ?: PushyHostResult(@"failed", @"internal_error", nil, NO); -#endif -} - -+ (void)recordJsRound:(NSString *)config { - @synchronized (RCTPushyOrchestrator.class) { - pushyJsCompletedConfig = [config copy]; - } -} - -// True when JS already obtained a valid response in this process for a config -// equivalent to the one the native round would use (§10.3): the delayed round is -// then a duplicate request. Only the scheduled round asks — the crash-rescue -// path still runs, JS is dead by then. -+ (BOOL)hasJsRound { - NSString *jsConfig; - @synchronized (RCTPushyOrchestrator.class) { - jsConfig = pushyJsCompletedConfig; - } - if (jsConfig == nil) { - return NO; - } - return PushyConfigsEquivalent([PushyDefaults() stringForKey:keyNativeConfig], jsConfig); -} - -// Runs the process's single round on the calling thread if nobody has -// started it yet. deadlineUptime > 0 (crash rescue) caps every HTTP call and -// download phase to the remaining budget. -+ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime { - if ([self claimRound]) { - [self runClaimedRoundWithDeadline:deadlineUptime]; - } -} - -// Claims this process's single round. Missing/disabled configuration is a -// preflight skip, not a used round. A YES must be followed by exactly one -// runClaimedRoundWithDeadline:. -+ (BOOL)claimRound { - if (![self hasRunnableConfig]) { - return NO; - } - bool expected = false; - return pushyRoundStarted.compare_exchange_strong(expected, true); -} - -+ (void)runClaimedRoundWithDeadline:(NSTimeInterval)deadlineUptime { - @try { - [self runOnce:pushyLaunchRolledBackForHold deadline:deadlineUptime]; - } @catch (NSException *exception) { - // The rescue path must never take the app down with it. - RCTLogWarn(@"RCTPushy -- native sync crashed: %@", exception.reason); - pushyHostRoundResult = PushyHostResult(@"failed", @"internal_error", nil, NO); - } @finally { - pushyRoundCompleted.store(true); - dispatch_semaphore_signal(pushyRoundDone); - dispatch_group_leave(pushyHostRoundGroup); - } -} - -// Crash-rescue entry (§11.3), called from the handler's worker queue while -// the uncaught-exception handler holds the dying process. Ensures this -// process's round runs to completion within the budget, then activates a -// downloaded-but-unactivated version if one exists — the last chance before -// the process is gone. -+ (void)runHoldRoundWithDeadline:(NSTimeInterval)deadlineUptime { - pushyCrashHoldActive.store(true); - [self startRoundWithDeadline:deadlineUptime]; - if (pushyRoundStarted.load() && !pushyRoundCompleted.load()) { - NSTimeInterval remaining = deadlineUptime - PushyMonotonicNow(); - if (remaining > 0) { - dispatch_semaphore_wait(pushyRoundDone, dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(remaining * NSEC_PER_SEC))); - } - } - [self activatePendingVersion]; -} - -// The alert-strategy variant of the §10.7 hole: the round downloaded a fix -// but deferred activation to JS, and JS is now dead. Activation is local and -// bounded (a state switch under the commit lock). -+ (void)activatePendingVersion { - NSString *hash = nil; - uint64_t generation = 0; - uint64_t configGeneration = 0; - @synchronized (self) { - hash = pushyUnactivatedHash; - generation = pushyUnactivatedGeneration; - configGeneration = pushyUnactivatedConfigGeneration; - } - if (hash == nil) { - return; - } - // Unlike Android's switchVersion, PushySwitchVersionLocked does not - // validate the version directory; never point the next launch at bytes - // that are gone (e.g. wiped by a reset whose generation bump we would - // catch below anyway, or by cleanup). - NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash]; - if (!PushyHasCompletedVersionAtPath(versionDir, hash)) { - NSLog(@"RCTPushy -- crash hold: version %@ no longer on disk, dropping activation", hash); - return; - } - NSDictionary *hashInfoEntry = nil; - NSString *existingInfo = [PushyDefaults() stringForKey:PushyHashInfoKey(hash)]; - NSData *existingData = [existingInfo dataUsingEncoding:NSUTF8StringEncoding]; - id parsed = existingData == nil ? nil : - [NSJSONSerialization JSONObjectWithData:existingData options:0 error:nil]; - if ([parsed isKindOfClass:[NSDictionary class]]) { - NSMutableDictionary *merged = [parsed mutableCopy]; - merged[PushyInfoCrashHold()] = @YES; - hashInfoEntry = @{@"hash": hash, @"info": merged}; - } - BOOL committed = [self commitRoundWithGeneration:generation - configGeneration:configGeneration - hashInfo:hashInfoEntry - activate:hash - responseText:nil - request:nil - config:nil - responseAt:0 - activated:NULL]; - if (committed) { - @synchronized (self) { - pushyUnactivatedHash = nil; - } - NSLog(@"RCTPushy -- crash hold: activated downloaded version %@", hash); - } else { - NSLog(@"RCTPushy -- crash hold: reset or new config since download, dropping activation"); - } -} - -// A bare module instance drives the existing download/patch pipeline without -// a bridge: the file queue is process-global and progress events are gated on -// hasListeners (never set without a bridge). -+ (RCTPushy *)engine { - static RCTPushy *engine; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - engine = [[RCTPushy alloc] init]; - }); - return engine; -} - -+ (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime { - // Generations first, config second: a config replaced in between is - // caught by the commit's generation check. - const uint64_t resetGeneration = pushyResetGeneration.load(); - const uint64_t configGeneration = pushyNativeConfigGeneration.load(); - pushyHostRoundGeneration = resetGeneration; - pushyHostRoundConfigGeneration = configGeneration; - pushyHostRoundResult = PushyHostResult(@"failed", @"check_failed", nil, NO); - NSUserDefaults *defaults = PushyDefaults(); - PushyRunnableConfig config; - NSString *reason = nil; - BOOL runnable = PushyLoadRunnableConfig(&config, &reason); - pushyHostRoundConfig = config.json; - if (!runnable) { - pushyHostRoundResult = PushyUnrunnableConfigResult(reason); - return; - } - // Preserve the interrupted-round breadcrumb and reset-safe atomic commit. - [defaults setObject:@YES forKey:keyNativeCheckIncomplete]; - @try { - [self runConfiguredRound:config.value - configJson:config.json - appKey:config.appKey - launchRolledBackVersion:launchRolledBackVersion - resetGeneration:resetGeneration - configGeneration:configGeneration - deadline:deadlineUptime]; - } @finally { - [defaults removeObjectForKey:keyNativeCheckIncomplete]; - } -} - -+ (void)runConfiguredRound:(const flowjson::Value &)config - configJson:(NSString *)configJson - appKey:(NSString *)appKey - launchRolledBackVersion:(NSString *)launchRolledBackVersion - resetGeneration:(uint64_t)resetGeneration - configGeneration:(uint64_t)configGeneration - deadline:(NSTimeInterval)deadlineUptime { - NSUserDefaults *defaults = PushyDefaults(); - NSString *packageVersion = - PushyFromStdString(config.Get("packageVersion").AsString()); - if (packageVersion.length == 0) { - packageVersion = [RCTPushy packageVersion]; - } - - __block NSString *currentVersion = nil; - PushyWithStateLock(^{ - pushy::state::State state = PushyStateFromDefaults(PushyDefaults()); - currentVersion = PushyFromStdString(state.current_version); - }); - NSString *rolledBackVersion = launchRolledBackVersion; - NSString *uuid = [defaults stringForKey:keyUuid] ?: @""; - - flowjson::Value identity = flowjson::Value::Object(); - identity.Set("packageVersion", - flowjson::Value::String(PushyToStdString(packageVersion))); - if (currentVersion != nil) { - identity.Set("currentVersion", - flowjson::Value::String(PushyToStdString(currentVersion))); - } - identity.Set("uuid", flowjson::Value::String(PushyToStdString(uuid))); - if (rolledBackVersion != nil) { - identity.Set("rolledBackVersion", - flowjson::Value::String(PushyToStdString(rolledBackVersion))); - } - - flowjson::Value cInfo = flowjson::Value::Object(); - cInfo.Set("rnu", config.Get("rnu")); - cInfo.Set("rn", config.Get("rn")); - cInfo.Set("os", flowjson::Value::String(PushyToStdString([NSString - stringWithFormat:@"%@ %@", PushyOsName, PushyOsVersion()]))); - cInfo.Set("uuid", flowjson::Value::String(PushyToStdString(uuid))); - - flowjson::Value input = flowjson::Value::Object(); - input.Set("packageVersion", identity.Get("packageVersion")); - if (currentVersion != nil) { - input.Set("currentVersion", identity.Get("currentVersion")); - } - input.Set("buildTime", - flowjson::Value::String(PushyToStdString([RCTPushy buildTime]))); - input.Set("cInfo", cInfo); - input.Set("supportedDiffVersion", - flowjson::Value::Number(pushy::hbc::kSupportedDiffVersion)); - // Cache-only while a crash is being held (§11.3 budget) or the launch is - // blocked on the tvOS purged-version restore; computed (and shared with JS - // getBundleHash) on the ordinary delayed round. - input.Set("bundleHash", - flowjson::Value::String(PushyToStdString(PushyBundleHashSync( - !(pushyCrashHoldActive.load() || pushyPurgeRestoreActive.load()))))); - - std::string bodyJson = - flowjson::Stringify(flowcore::BuildRequestBody(input)); - NSString *body = [NSString stringWithUTF8String:bodyJson.c_str()]; - if (body == nil) { - RCTLogWarn(@"RCTPushy -- native sync: request body is not valid UTF-8"); - pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_request", nil, NO); - return; - } - - NSString *responseText = [self runQueryRequest:config - appKey:appKey - body:body - deadline:deadlineUptime]; - if (responseText == nil) { - RCTLogInfo(@"RCTPushy -- native sync: no endpoint reachable, giving up until next launch"); - return; - } - long long responseAtSeconds = (long long)[[NSDate date] timeIntervalSince1970]; - - flowjson::Value decision = flowcore::HandleResponse( - PushyToStdString(responseText), identity, false, - config.Get("afterDownload").AsString()); - if (decision.Get("action").AsString() != "download") { - BOOL committed = [self commitRoundWithGeneration:resetGeneration - configGeneration:configGeneration - hashInfo:nil - activate:nil - responseText:responseText - request:body - config:configJson - responseAt:responseAtSeconds - activated:NULL]; - pushyHostRoundResult = committed - ? PushyHostResult(PushyStatusNone(), PushyFromStdString(decision.Get("reason").AsString()), nil, NO) - : PushyHostResult(@"cancelled", @"reset", nil, NO); - RCTLogInfo(@"RCTPushy -- native sync: nothing to do (%s)", - decision.Get("reason").AsString().c_str()); - return; - } - NSString *hash = PushyFromStdString(decision.Get("hash").AsString()); - if (!PushyIsSafePathComponent(hash)) { - pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_response", nil, NO); - return; - } - - NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash]; - BOOL downloaded = PushyHasCompletedVersionAtPath(versionDir, hash); - if (!downloaded) { - downloaded = [self performAttempts:decision.Get("attempts") - hash:hash - originHash:currentVersion - deadline:deadlineUptime]; - } - if (!downloaded) { - BOOL committed = [self commitRoundWithGeneration:resetGeneration - configGeneration:configGeneration - hashInfo:nil - activate:nil - responseText:responseText - request:body - config:configJson - responseAt:responseAtSeconds - activated:NULL]; - pushyHostRoundResult = committed - ? PushyHostResult(@"failed", @"download_failed", nil, NO) - : PushyHostResult(@"cancelled", @"reset", nil, NO); - return; - } - - // Persist name/description/metaInfo alongside the version, mirroring the - // JS side's setLocalHashInfo after a successful download. - const flowjson::Value &info = decision.Get("info"); - NSMutableDictionary *versionInfo = [NSMutableDictionary dictionary]; - for (const char *key : {"name", "description", "metaInfo"}) { - if (info.Get(key).IsString()) { - versionInfo[@(key)] = PushyFromStdString(info.Get(key).AsString()) ?: @""; - } - } - // A forceBoot activation is the brick-rescue path: mark it in the - // persisted info so JS can report force_boot_rescue when this version - // survives to markSuccess. Only the server-sent directive counts — a - // silent-strategy activation is ordinary delivery. - if (info.Get("config").Get("forceBoot").Truthy()) { - versionInfo[PushyInfoForceBoot()] = @YES; - } - if (pushyCrashHoldActive.load()) { - versionInfo[PushyInfoCrashHold()] = @YES; - } - // Silent strategies or a server-marked forceBoot version (per-version - // remote override — the brick rescue) activate for the next launch; - // otherwise activation stays with the JS side (§6/§10.1). Unless a crash - // is being held: JS is dead, deferring to it would leave the fix on disk - // forever (§11.3). Or the launch is blocked on reinstalling a version - // tvOS purged: JS has not started, and the whole point is to boot it — - // commitRoundWithGeneration drops that one once the restore window closed. - BOOL activate = decision.Get("activate").Truthy() || pushyCrashHoldActive.load() - || pushyPurgeRestoreActive.load(); - BOOL activated = NO; - BOOL committed = [self commitRoundWithGeneration:resetGeneration - configGeneration:configGeneration - hashInfo:@{@"hash": hash, @"info": versionInfo} - activate:activate ? hash : nil - responseText:responseText - request:body - config:configJson - responseAt:responseAtSeconds - activated:&activated]; - activate = activated; - if (!committed) { - RCTLogInfo(@"RCTPushy -- native sync: reset during round, dropping result"); - } else if (activate) { - @synchronized (self) { - pushyUnactivatedHash = nil; - } - RCTLogInfo(@"RCTPushy -- native sync: downloaded %@ and set for next launch", hash); - } else { - // Remembered so a crash later in this process can still activate it - // (activatePendingVersion) — JS never will. - @synchronized (self) { - pushyUnactivatedHash = [hash copy]; - pushyUnactivatedGeneration = resetGeneration; - pushyUnactivatedConfigGeneration = configGeneration; - } - RCTLogInfo(@"RCTPushy -- native sync: downloaded %@, activation left to JS", hash); - } - pushyHostRoundResult = committed - ? PushyHostResult(@"downloaded", @"", hash, activate) - : PushyHostResult(@"cancelled", @"reset", nil, NO); -} - -// Everything a round persists — version info, the activation, the response -// cache — is written inside ONE state-lock acquisition that first re-checks the -// reset and config generations. resetToPackagedBundle bumps that generation under the same -// lock, so there is no compare-and-act window: either the whole round commits, -// or the reset wins and none of it does. -+ (BOOL)commitRoundWithGeneration:(uint64_t)generation - configGeneration:(uint64_t)configGeneration - hashInfo:(NSDictionary *)hashInfoEntry - activate:(NSString *)hashToActivate - responseText:(NSString *)responseText - request:(NSString *)requestBody - config:(NSString *)configJson - responseAt:(long long)responseAtSeconds - activated:(BOOL *)activatedOut { - // responseText is nil for the crash handler's activation-only commit - // (activatePendingVersion): no round ran, so there is no cache to write. - NSData *cacheData = nil; - if (responseText != nil) { - NSDictionary *cacheEntry = @{ - @"ts": @(responseAtSeconds), - @"body": responseText, - @"request": requestBody, - @"config": configJson, - }; - cacheData = [NSJSONSerialization dataWithJSONObject:cacheEntry options:0 error:nil]; - } - __block BOOL committed = NO; - __block BOOL activated = NO; - PushyWithStateLock(^{ - if (pushyResetGeneration.load() != generation) { - return; - } - // The config the round ran with was replaced (persistConfiguration - // bumps this under the same lock). - if (pushyNativeConfigGeneration.load() != configGeneration) { - return; - } - NSString *activation = hashToActivate; - NSDictionary *info = hashInfoEntry[@"info"]; - // The tvOS purged-version restore round (a round commit, not the - // crash handler's activation-only one): it may take over this launch - // only while +bundleURL still waits for it; after that the version is - // left to JS unless a crash is being held (JS is dead then). - if (responseText != nil && pushyPurgeRestoreActive.load()) { - if (pushyPurgeRestoreWindowOpen) { - if (activation != nil && info != nil) { - NSMutableDictionary *marked = [info mutableCopy]; - marked[@"purgeRestore"] = @YES; - info = marked; - } - } else if (!pushyCrashHoldActive.load()) { - activation = nil; - } - } - NSUserDefaults *defaults = PushyDefaults(); - if (hashInfoEntry != nil) { - NSData *infoData = [NSJSONSerialization dataWithJSONObject:info - options:0 - error:nil]; - if (infoData != nil) { - [defaults setObject:[[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding] - forKey:PushyHashInfoKey(hashInfoEntry[@"hash"])]; - } - } - if (activation != nil) { - PushySwitchVersionLocked(activation); - activated = YES; - } - if (cacheData != nil) { - [defaults setObject:[[NSString alloc] initWithData:cacheData encoding:NSUTF8StringEncoding] - forKey:keyNativeCheckCache]; - } - committed = YES; - }); - if (activatedOut != NULL) { - *activatedOut = activated; - } - return committed; -} - -// Sequential fallback over the ordered candidates (§5.1): one request at a -// time with its own timeout; after the configured round fails, queryUrls -// discovery merges remote candidates (excluding the already-tried) for one -// more round. No hedged race on purpose — this path is latency-insensitive. -// Per-request timeout, capped to the crash-rescue budget when one is active. -// <= 0 means the budget is gone and the round must stop issuing requests. -static NSTimeInterval PushyQueryRequestTimeout(NSTimeInterval deadlineUptime) { - if (deadlineUptime <= 0) { - return 10; - } - return MIN(10, deadlineUptime - PushyMonotonicNow()); -} - -+ (NSString *)runQueryRequest:(const flowjson::Value &)config - appKey:(NSString *)appKey - body:(NSString *)body - deadline:(NSTimeInterval)deadlineUptime { - NSMutableSet *tried = [NSMutableSet set]; - const NSUInteger maxHttpAttempts = 8; - __block NSUInteger httpAttempts = 0; - // Spends one attempt of the shared budget. NO once the attempt cap or the - // deadline is exhausted (the round must stop issuing requests); the - // per-request timeout lands in *timeout otherwise. - BOOL (^takeAttempt)(NSTimeInterval *) = ^BOOL(NSTimeInterval *timeout) { - if (httpAttempts++ >= maxHttpAttempts) { - return NO; - } - *timeout = PushyQueryRequestTimeout(deadlineUptime); - return *timeout > 0; - }; - // Tries each not-yet-tried endpoint base in order and returns the first - // valid response; *stop is set when the budget ran out. - NSString *(^queryEndpoints)(const flowjson::Value &, BOOL *) = - ^NSString *(const flowjson::Value &endpoints, BOOL *stop) { - for (const auto &endpoint : endpoints.elements()) { - NSString *base = PushyNormalizeEndpointBase( - PushyFromStdString(endpoint.AsString())); - if (base.length == 0 || [tried containsObject:base]) { - continue; - } - NSTimeInterval timeout = 0; - if (!takeAttempt(&timeout)) { - *stop = YES; - return nil; - } - [tried addObject:base]; - NSString *response = PushyHttpRequest( - [NSString stringWithFormat:@"%@%@%@", base, RCTPushyQueryPath(), appKey], - @"POST", body, timeout); - if (PushyIsValidResponse(response)) { - return response; - } - } - return nil; - }; - - BOOL stop = NO; - double sample = arc4random() / 4294967296.0; - NSString *response = queryEndpoints( - flowcore::OrderEndpointCandidates(config.Get("endpoints"), sample), &stop); - if (response != nil || stop) { - return response; - } - for (const auto &queryUrl : config.Get("queryUrls").elements()) { - NSString *listUrl = PushyFromStdString(queryUrl.AsString()); - if (listUrl == nil) { - continue; - } - NSTimeInterval listTimeout = 0; - if (!takeAttempt(&listTimeout)) { - return nil; - } - NSString *listText = PushyHttpRequest(listUrl, @"GET", nil, listTimeout); - if (listText == nil) { - continue; - } - bool ok = false; - flowjson::Value remote = flowjson::Parse(PushyToStdString(listText), &ok); - if (!ok || !remote.IsArray()) { - continue; - } - // One successfully fetched remote list is enough. - return queryEndpoints(remote, &stop); - } - return nil; -} - -+ (BOOL)performAttempts:(const flowjson::Value &)attempts - hash:(NSString *)hash - originHash:(NSString *)originHash - deadline:(NSTimeInterval)holdDeadline { - RCTPushy *engine = [self engine]; - // Crash-rescue budget caps every phase; 0 keeps the normal 10min windows. - NSTimeInterval incrementalDeadline = PushyMonotonicNow() + 600; - if (holdDeadline > 0) { - incrementalDeadline = MIN(incrementalDeadline, holdDeadline); - } - NSTimeInterval fullDeadline = 0; - for (const auto &attempt : attempts.elements()) { - const std::string &type = attempt.Get("type").AsString(); - PushyType pushyType = type == "diff" ? PushyTypePatchFromPpk - : type == "pdiff" ? PushyTypePatchFromPackage - : PushyTypeFullDownload; - if (pushyType == PushyTypePatchFromPpk && originHash.length == 0) { - continue; // diff patches from the running version; none running - } - BOOL isFullAttempt = pushyType == PushyTypeFullDownload; - if (isFullAttempt && fullDeadline == 0) { - // diff/pdiff cannot starve the last-resort full download. - fullDeadline = PushyMonotonicNow() + 600; - if (holdDeadline > 0) { - fullDeadline = MIN(fullDeadline, holdDeadline); - } - } - NSTimeInterval deadline = isFullAttempt ? fullDeadline : incrementalDeadline; - for (const auto &urlValue : attempt.Get("urls").elements()) { - NSString *url = PushyFromStdString(urlValue.AsString()); - if (url == nil) { - continue; - } - NSTimeInterval remaining = deadline - PushyMonotonicNow(); - if (remaining <= 0) { - if (isFullAttempt) { - return NO; - } - break; - } - NSMutableDictionary *options = - [@{ - PushyOptionUrlKey(): url, - @"hash": hash, - @"deadlineUptime": @(deadline), - } mutableCopy]; - if (pushyType == PushyTypePatchFromPpk) { - options[@"originHash"] = originHash; - } - dispatch_semaphore_t sem = dispatch_semaphore_create(0); - __block NSError *resultError = nil; - [engine performFetch:pushyType options:options callback:^(NSError *error) { - resultError = error; - dispatch_semaphore_signal(sem); - }]; - if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, - (int64_t)(remaining * NSEC_PER_SEC))) != 0) { - RCTLogWarn(@"RCTPushy -- native sync: %s attempt timed out", type.c_str()); - if (isFullAttempt) { - return NO; - } - break; - } - if (resultError == nil) { - return YES; - } - RCTLogInfo(@"RCTPushy -- native sync: %s attempt failed: %@", - type.c_str(), resultError.localizedDescription); - } - } - return NO; -} - -@end diff --git a/ios/RCTPushy/RCTPushyInternal.h b/ios/RCTPushy/RCTPushyInternal.h new file mode 100644 index 00000000..28c02a42 --- /dev/null +++ b/ios/RCTPushy/RCTPushyInternal.h @@ -0,0 +1,117 @@ +// Internal to the RCTPushy pod: what RCTPushy.mm (module, install pipeline, +// launch resolution) and RCTPushyOrchestrator.mm (native round) share. Not a +// public header. +#pragma once + +#import "RCTPushy.h" +#import +#import + +#include +#include + +#include "../../cpp/patch_core/state_core.h" + +// RN's log macros pass __FILE__, which would embed this file's absolute build +// path in the binary; log with the bare file name instead. +#if RCTLOG_ENABLED +#undef _RCTLog +#define _RCTLog(lvl, ...) _RCTLogNativeInternal(lvl, __FILE_NAME__, __LINE__, __VA_ARGS__) +#endif + +// Shared across the pod's translation units, never exported from a dynamic +// framework. +#define PUSHY_HIDDEN __attribute__((visibility("hidden"))) + +typedef NS_ENUM(NSInteger, PushyType) { + PushyTypeFullDownload = 1, + PushyTypePatchFromPackage = 2, + PushyTypePatchFromPpk = 3, +}; + +@interface RCTPushy () +- (void)fetchPackage:(PushyType)type + options:(NSDictionary *)options + resolver:(RCTPromiseResolveBlock)resolve + rejecter:(RCTPromiseRejectBlock)reject; +- (void)performFetch:(PushyType)type + options:(NSDictionary *)options + callback:(void (^)(NSError *error))callback; +- (NSError *)restartBridgeWithReason:(NSString *)reason; +- (void)unzipDownloadedPackage:(NSString *)zipFilePath + hash:(NSString *)hash + type:(PushyType)type + originHash:(NSString *)originHash + callback:(void (^)(NSError *error))callback; +- (void)finishDownloadedPackage:(NSString *)hash + type:(PushyType)type + originHash:(NSString *)originHash + callback:(void (^)(NSError *error))callback; +- (void)applyDeltaForHash:(NSString *)hash + type:(PushyType)type + fromBundle:(NSString *)bundleOrigin + source:(NSString *)sourceOrigin + callback:(void (^)(NSError *error))callback; +- (BOOL)switchVersion:(NSString *)hash error:(NSError **)error; +- (BOOL)ensureDirectoryExistsAtPath:(NSString *)path; ++ (void)excludeFromBackup:(NSString *)path; +- (void)unzipFileAtPath:(NSString *)path + toDestination:(NSString *)destination + completionHandler:(void (^)(NSError *error))completionHandler; ++ (NSString *)downloadDir; ++ (NSURL *)binaryBundleURL; ++ (NSString *)packageVersion; ++ (NSString *)buildTime; +@end + +// Native cold-start update check (NATIVE_CHECKUPDATE_DESIGN §10): runs once +// per process, a few seconds after launch, entirely independent of the app +// bundle — this is what lets a bricked hot update be replaced on the next +// launch. Decisions come from cpp/update_flow_core; this class is IO glue. +@interface RCTPushyOrchestrator : NSObject ++ (void)persistConfiguration:(NSString *)config; ++ (BOOL)hasRunnableConfig; ++ (NSDictionary *)prepareBundle; ++ (NSDictionary *)prepareBundleWithin:(NSTimeInterval)timeout; ++ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion; +#if TARGET_OS_TV && !DEBUG ++ (BOOL)restorePurgedLaunch:(NSString *)purgedVersion + rolledBack:(NSString *)launchRolledBackVersion; +#endif ++ (void)recordJsRound:(NSString *)config; ++ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime; ++ (BOOL)claimRound; ++ (void)runClaimedRoundWithDeadline:(NSTimeInterval)deadlineUptime; ++ (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime; ++ (void)runHoldRoundWithDeadline:(NSTimeInterval)deadlineUptime; ++ (BOOL)commitRoundWithGeneration:(uint64_t)generation + configGeneration:(uint64_t)configGeneration + hashInfo:(NSDictionary *)hashInfoEntry + activate:(NSString *)hashToActivate + responseText:(NSString *)responseText + request:(NSString *)requestBody + config:(NSString *)configJson + responseAt:(long long)responseAtSeconds + activated:(BOOL *)activatedOut; +@end + +extern PUSHY_HIDDEN NSString *const keyUuid; +extern PUSHY_HIDDEN NSString *const keyNativeConfig; +extern PUSHY_HIDDEN NSString *const keyNativeCheckCache; +extern PUSHY_HIDDEN NSString *const keyNativeCheckIncomplete; +extern PUSHY_HIDDEN std::atomic pushyResetGeneration; +PUSHY_HIDDEN NSDictionary *PushyHostResult(NSString *status, NSString *reason, NSString *hash, BOOL activated); +PUSHY_HIDDEN NSTimeInterval PushyMonotonicNow(void); +PUSHY_HIDDEN std::string PushyToStdString(NSString *value); +PUSHY_HIDDEN NSString *PushyFromStdString(const std::string &value); +PUSHY_HIDDEN NSUserDefaults *PushyDefaults(void); +PUSHY_HIDDEN void PushyWithStateLock(void (NS_NOESCAPE ^block)(void)); +PUSHY_HIDDEN NSString *PushyHashInfoKey(NSString *hash); +PUSHY_HIDDEN BOOL PushyIsSafePathComponent(NSString *value); +PUSHY_HIDDEN BOOL PushyHasCompletedVersionAtPath(NSString *versionDir, NSString *hash); +PUSHY_HIDDEN void PushySwitchVersionLocked(NSString *hash); +PUSHY_HIDDEN NSString *PushyBundleHashSync(BOOL computeIfMissing); +PUSHY_HIDDEN NSString *PushyOptionUrlKey(void); +PUSHY_HIDDEN NSString *PushyRevealOnce(NSString *__strong *slot, dispatch_once_t *once, const char *hex); + +PUSHY_HIDDEN pushy::state::State PushyStateFromDefaults(NSUserDefaults *defaults); diff --git a/ios/RCTPushy/RCTPushyOrchestrator.mm b/ios/RCTPushy/RCTPushyOrchestrator.mm new file mode 100644 index 00000000..7ebcdee4 --- /dev/null +++ b/ios/RCTPushy/RCTPushyOrchestrator.mm @@ -0,0 +1,1258 @@ +#import "RCTPushyInternal.h" +#import "RCTPushyConfiguration.h" +#include "../../cpp/patch_core/error_codes.h" +#include "../../cpp/patch_core/hbc_transform_wire.h" +#include "../../cpp/update_flow_core/flow_json.h" +#include "../../cpp/update_flow_core/update_flow_core.h" + +#include +#if TARGET_OS_TV +#import +#include +#include +#endif + +// Version-info flags and the no-op status read by JS, stored encoded +// (RCTPushyRevealText) so they do not appear as plain binary strings. +static NSString *PushyInfoCrashHold(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "3905f5c2a6b96d56212a19"); +} +static NSString *PushyInfoForceBoot(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "3c18e6d2aba9674a360d19ead5a695"); +} +static NSString *PushyStatusNone(void) { + static NSString *value; + static dispatch_once_t once; + return PushyRevealOnce(&value, &once, "3418c1c1aa8a7c40"); +} + +// cInfo.os label prefix; must match the JS side (core.ts) so the server +// buckets tvOS devices apart from iOS ones. +#if TARGET_OS_TV +static NSString * const PushyOsName = @"tvos"; +#else +static NSString * const PushyOsName = @"ios"; +#endif + +// One round per process, whoever starts it first — the delayed cold-start +// path or the crash-rescue thread (§11.3). The semaphore lets the rescue +// wait out an in-flight round instead of racing it. +static std::atomic pushyRoundStarted{false}; +static std::atomic pushyRoundCompleted{false}; +// Flipped the moment a crash is being held. JS is dead from that point on, +// so there is no second decision maker: the round force-activates whatever +// it downloads (§11.3). +static std::atomic pushyCrashHoldActive{false}; +static std::atomic pushyHoldAttempted{false}; +// Set when this process's round is the tvOS purged-version restore. +static std::atomic pushyPurgeRestoreActive{false}; +// True while +bundleURL waits for that restore. Only then may the round +// activate what it downloads on its own (JS has not started to decide), since +// the launch resolves the bundle again afterwards. Closed once the wait ends, +// timeout or not; guarded by the state lock that every activation commits +// under, so a commit lands either before the close (and launches) or after it +// (and is left to JS) — never selected behind the packaged bundle's back. +static bool pushyPurgeRestoreWindowOpen = false; +static dispatch_semaphore_t pushyRoundDone; +static NSString *pushyLaunchRolledBackForHold = nil; +// A version this process downloaded but left for JS to activate. If the +// process then crashes, JS will never activate it — the crash handler +// activates it directly (bounded local work, no network). The generation is +// the one the round committed under: a reset that lands afterwards bumps it, +// and the late activation must lose to that reset exactly like the round +// itself would. Guarded by @synchronized (RCTPushyOrchestrator class). +static NSString *pushyUnactivatedHash = nil; +static uint64_t pushyUnactivatedGeneration = 0; +static uint64_t pushyUnactivatedConfigGeneration = 0; +static NSUncaughtExceptionHandler *pushyPreviousExceptionHandler = NULL; +// ≈ process start: scheduleFromColdStart runs during the first bundleURL. +static NSTimeInterval pushyProcessAnchorUptime = 0; +// Config JSON for which JS reported a completed check in this process +// (markJsCheckCompleted). Process-scoped by design. Guarded by +// @synchronized (RCTPushyOrchestrator class). +static NSString *pushyJsCompletedConfig = nil; +// The group supplements (rather than consumes) the crash-rescue semaphore. +static dispatch_group_t pushyHostRoundGroup; +static std::atomic pushySyncReady{false}; +static NSDictionary *pushyHostRoundResult = nil; +static NSString *pushyHostRoundConfig = nil; +static uint64_t pushyHostRoundGeneration = 0; +// Bumped by persistConfiguration when the config really changes (C1 +// equivalence fails); resets keep pushyResetGeneration to themselves. +static std::atomic pushyNativeConfigGeneration{0}; +static uint64_t pushyHostRoundConfigGeneration = 0; + +static const NSTimeInterval kPushyHoldTriggerUptime = 60; +static const NSTimeInterval kPushyHoldBudgetBackgroundThread = 10; +// A held main thread freezes UI teardown; stay well under the watchdog. +static const NSTimeInterval kPushyHoldBudgetMainThread = 3.5; +// The purged-version restore blocks +bundleURL, usually inside +// application:didFinishLaunching: — stay clear of the launch watchdog. The +// budget is measured from process start; with less than the minimum left the +// restore is not attempted. +#if TARGET_OS_TV && !DEBUG +static const NSTimeInterval kPushyPurgeRestoreBudget = 12; +static const NSTimeInterval kPushyPurgeRestoreMinimumBudget = 2; + +// Seconds since the kernel started this process; 0 when unknown. +static NSTimeInterval PushyProcessElapsed(void) { + struct kinfo_proc info; + size_t size = sizeof(info); + int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()}; + if (sysctl(mib, 4, &info, &size, NULL, 0) != 0 || size == 0) { + return 0; + } + struct timeval now; + gettimeofday(&now, NULL); + const struct timeval start = info.kp_proc.p_starttime; + NSTimeInterval elapsed = (now.tv_sec - start.tv_sec) + + (now.tv_usec - start.tv_usec) / 1e6; + return elapsed > 0 ? elapsed : 0; +} + +// NO only when the system positively reports no usable network path; an +// unknown state (no answer within the short wait) lets the restore try. +static BOOL PushyHasNetworkPath(void) { + dispatch_queue_t queue = dispatch_queue_create("cn.reactnative.pushy.path", DISPATCH_QUEUE_SERIAL); + nw_path_monitor_t monitor = nw_path_monitor_create(); + dispatch_semaphore_t answered = dispatch_semaphore_create(0); + __block nw_path_status_t status = nw_path_status_invalid; + nw_path_monitor_set_queue(monitor, queue); + nw_path_monitor_set_update_handler(monitor, ^(nw_path_t path) { + BOOL first = status == nw_path_status_invalid; + status = nw_path_get_status(path); + if (first) { + dispatch_semaphore_signal(answered); + } + }); + nw_path_monitor_start(monitor); + dispatch_semaphore_wait(answered, dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC / 2)); + nw_path_monitor_cancel(monitor); + __block nw_path_status_t result; + dispatch_sync(queue, ^{ + result = status; + }); + return result != nw_path_status_unsatisfied; +} +#endif +// prepareBundleWithCompletion: (no explicit timeout) waits this long at most: +// past the longest round (8 query requests, then two 600s download phases). +static const NSTimeInterval kPushyHostPrepareDefaultTimeout = 1800; + +static void PushyMaybeHoldProcess(void) { + // Once per process; a second crashing thread passes straight through + // instead of waiting behind the first (§11.3: prefer under-rescuing over + // wedging the teardown). + bool expected = false; + if (!pushyHoldAttempted.compare_exchange_strong(expected, true)) { + return; + } + NSTimeInterval uptime = PushyMonotonicNow() - pushyProcessAnchorUptime; + bool roundInFlight = pushyRoundStarted.load() && !pushyRoundCompleted.load(); + // Early crashes are the brick signature; a crash with an in-flight round + // is worth finishing regardless of uptime. Everything else is an ordinary + // crash whose UX must not be delayed. + if (uptime >= kPushyHoldTriggerUptime && !roundInFlight) { + return; + } + NSTimeInterval budget = [NSThread isMainThread] + ? kPushyHoldBudgetMainThread + : kPushyHoldBudgetBackgroundThread; + NSTimeInterval deadline = PushyMonotonicNow() + budget; + NSLog(@"RCTPushy -- crash hold: holding process for up to %.1fs " + @"(uptime %.1fs)", budget, uptime); + + // The rescue runs on its own queue and the dying thread only waits with a + // hard timeout: even a deadlocked rescue (a thread that died holding a + // lock — uncaught ObjC exceptions skip @finally) can only delay the + // process death, never prevent it. + dispatch_semaphore_t done = dispatch_semaphore_create(0); + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + @try { + [RCTPushyOrchestrator runHoldRoundWithDeadline:deadline]; + } @catch (NSException *exception) { + NSLog(@"RCTPushy -- crash hold failed: %@", exception.reason); + } + dispatch_semaphore_signal(done); + }); + if (dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(budget * NSEC_PER_SEC))) != 0) { + NSLog(@"RCTPushy -- crash hold: budget exhausted, letting go"); + } +} + +// Crash-moment brick rescue (§11): when the app is dying of an uncaught +// exception during startup (RCTFatal raises NSException in release), the +// process is still alive and JS will never run again — a natural, +// false-positive-free window to finish the cold-start check. The previous +// handler (crash reporters chain the same way) always runs afterwards. +static void PushyCrashHoldExceptionHandler(NSException *exception) { + @try { + PushyMaybeHoldProcess(); + } @catch (NSException *inner) { + // The dying process owes the previous handler its turn no matter + // what the rescue did. + } + if (pushyPreviousExceptionHandler != NULL) { + pushyPreviousExceptionHandler(exception); + } +} + +static void PushyInstallCrashHoldHandler(void) { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pushyPreviousExceptionHandler = NSGetUncaughtExceptionHandler(); + NSSetUncaughtExceptionHandler(&PushyCrashHoldExceptionHandler); + }); +} + +#pragma mark - native cold-start check orchestration + +// Refuses an https→http downgrade before the redirected request — and with +// it the POST body (appKey, uuid, versions) — leaves the device. The shared +// session would follow first and only the response could be rejected; ATS +// blocks plaintext by default, this covers apps that opted out with +// NSAllowsArbitraryLoads. Same rule as RCTPushyDownloader. Returning nil +// delivers the 3xx itself as the final response, which the status check in +// PushyHttpRequest then treats as a failed endpoint. +@interface PushyRequestRedirectGuard : NSObject +@end + +@implementation PushyRequestRedirectGuard + +- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task +willPerformHTTPRedirection:(NSHTTPURLResponse *)response + newRequest:(NSURLRequest *)request + completionHandler:(void (^)(NSURLRequest *))completionHandler +{ + NSString *originalScheme = [task.originalRequest.URL.scheme lowercaseString]; + NSString *nextScheme = [request.URL.scheme lowercaseString]; + if ([originalScheme isEqualToString:@"https"] && [nextScheme isEqualToString:@"http"]) { + completionHandler(nil); + return; + } + completionHandler(request); +} + +@end + +// One long-lived delegate session (not the shared one) so the redirect guard +// sees the 3xx before it is followed; completion handlers still receive the +// final response. Never invalidated: it lives as long as the process. +static NSURLSession *PushyRequestSession(void) { + static NSURLSession *session; + static dispatch_once_t once; + dispatch_once(&once, ^{ + session = [NSURLSession + sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] + delegate:[PushyRequestRedirectGuard new] + delegateQueue:nil]; + }); + return session; +} + +// Same shape as -[UIDevice systemVersion] ("17.4", "17.4.1"), which is +// main-thread-only UIKit; the round runs on a background queue. +static NSString *PushyOsVersion(void) { + NSOperatingSystemVersion version = [NSProcessInfo processInfo].operatingSystemVersion; + if (version.patchVersion > 0) { + return [NSString stringWithFormat:@"%ld.%ld.%ld", (long)version.majorVersion, + (long)version.minorVersion, (long)version.patchVersion]; + } + return [NSString stringWithFormat:@"%ld.%ld", (long)version.majorVersion, + (long)version.minorVersion]; +} + +// Blocking JSON HTTP round-trip on the orchestrator's utility thread. Returns +// the response body on 2xx, nil on any failure. The semaphore timeout is a +// backstop over the request's own timeoutInterval. +static NSString *PushyHttpRequest(NSString *urlString, NSString *method, + NSString *body, NSTimeInterval timeout) { + NSURL *url = [NSURL URLWithString:urlString]; + if (url == nil) { + return nil; + } + NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url]; + request.HTTPMethod = method; + request.timeoutInterval = timeout; + [request setValue:@"application/json" forHTTPHeaderField:@"Accept"]; + if (body != nil) { + [request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"]; + request.HTTPBody = [body dataUsingEncoding:NSUTF8StringEncoding]; + } + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + __block NSString *result = nil; + NSURLSessionDataTask *task = [PushyRequestSession() dataTaskWithRequest:request + completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { + NSHTTPURLResponse *httpResponse = + [response isKindOfClass:[NSHTTPURLResponse class]] + ? (NSHTTPURLResponse *)response + : nil; + NSInteger status = httpResponse.statusCode; + if (error == nil && httpResponse != nil && status >= 200 && status < 300 + && data != nil) { + result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + } + dispatch_semaphore_signal(sem); + }]; + [task resume]; + if (dispatch_semaphore_wait( + sem, dispatch_time(DISPATCH_TIME_NOW, + (int64_t)((timeout + 5) * NSEC_PER_SEC))) != 0) { + [task cancel]; + return nil; + } + return result; +} + +static NSString *PushyNormalizeEndpointBase(NSString *base) { + while ([base hasSuffix:@"/"]) { + base = [base substringToIndex:base.length - 1]; + } + return base; +} + +static BOOL PushyIsValidResponse(NSString *responseText) { + if (responseText == nil) { + return NO; + } + // Shared schema rule (update_flow_core::IsValidResponse): a 200 with + // `{"error": ...}` is a failed endpoint, not a verdict. + return flowcore::IsValidResponse(PushyToStdString(responseText)) ? YES : NO; +} + +// Canonical config equality, shared with the JS, Android and Harmony sides: +// two config JSON strings are equivalent iff their canonical forms deep-equal. +// Canonical form: the parsed object with absent keys defaulted +// (disabled:false, queryUrls:[], rnu:"", rn:"", afterDownload:"none") and the +// endpoints/queryUrls entries stripped of trailing '/' and deduplicated in +// first-occurrence order; key order is irrelevant. Unparseable is never +// equivalent. Lets a host-owned config (sorted keys) and the same config as JS +// serializes it count as one, instead of a "change" that cancels the round. +static BOOL PushyJsonIsBool(id value) { + return [value isKindOfClass:NSNumber.class] + && CFGetTypeID((__bridge CFTypeRef)value) == CFBooleanGetTypeID(); +} + +static BOOL PushyJsonEqual(id a, id b) { + if (PushyJsonIsBool(a) != PushyJsonIsBool(b)) { + return NO; // false is not 0 + } + if ([a isKindOfClass:NSDictionary.class] && [b isKindOfClass:NSDictionary.class]) { + if ([a count] != [b count]) { + return NO; + } + for (id key in (NSDictionary *)a) { + id other = ((NSDictionary *)b)[key]; + if (other == nil || !PushyJsonEqual(((NSDictionary *)a)[key], other)) { + return NO; + } + } + return YES; + } + if ([a isKindOfClass:NSArray.class] && [b isKindOfClass:NSArray.class]) { + if ([a count] != [b count]) { + return NO; + } + for (NSUInteger i = 0; i < [a count]; i++) { + if (!PushyJsonEqual(((NSArray *)a)[i], ((NSArray *)b)[i])) { + return NO; + } + } + return YES; + } + return [a isEqual:b]; +} + +static id PushyCanonicalUrlList(id value) { + if (![value isKindOfClass:NSArray.class]) { + return value; + } + NSMutableArray *result = [NSMutableArray array]; + for (id item in (NSArray *)value) { + id entry = [item isKindOfClass:NSString.class] ? PushyNormalizeEndpointBase(item) : item; + BOOL seen = NO; + for (id existing in result) { + if (PushyJsonEqual(existing, entry)) { + seen = YES; + break; + } + } + if (!seen) { + [result addObject:entry]; + } + } + return result; +} + +static NSDictionary *PushyCanonicalConfig(NSString *json) { + NSData *data = [json dataUsingEncoding:NSUTF8StringEncoding]; + id object = data == nil ? nil + : [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + if (![object isKindOfClass:NSDictionary.class]) { + return nil; + } + NSMutableDictionary *config = [object mutableCopy]; + NSDictionary *defaults = @{@"disabled": @NO, @"queryUrls": @[], @"rnu": @"", + @"rn": @"", @"afterDownload": @"none"}; + for (NSString *key in defaults) { + if (config[key] == nil) { + config[key] = defaults[key]; + } + } + for (NSString *key in @[@"endpoints", @"queryUrls"]) { + if (config[key] != nil) { + config[key] = PushyCanonicalUrlList(config[key]); + } + } + return config; +} + +static BOOL PushyConfigsEquivalent(NSString *a, NSString *b) { + NSDictionary *left = a == nil ? nil : PushyCanonicalConfig(a); + NSDictionary *right = b == nil ? nil : PushyCanonicalConfig(b); + return left != nil && right != nil && PushyJsonEqual(left, right); +} + +// The persisted config a round may run with. Every gate (crash-hold handler +// install, round start, prepareBundle, the round itself) reads it through +// PushyLoadRunnableConfig, so they agree on what "configured" and "disabled" +// mean. disabled follows JS truthiness, like the flow core. +struct PushyRunnableConfig { + NSString *json; + flowjson::Value value; + NSString *appKey; +}; + +// YES when a round may run; otherwise *reason is not_configured, disabled or +// invalid_config. +static BOOL PushyLoadRunnableConfig(PushyRunnableConfig *config, NSString **reason) { + NSString *failure = nil; + NSString *json = [PushyDefaults() stringForKey:keyNativeConfig]; + config->json = [json copy]; + if (json.length == 0) { + failure = @"not_configured"; + } else { + bool ok = false; + flowjson::Value value = flowjson::Parse(PushyToStdString(json), &ok); + NSString *appKey = ok && value.IsObject() + ? PushyFromStdString(value.Get("appKey").AsString()) : nil; + if (!ok || !value.IsObject()) { + failure = @"invalid_config"; + } else if (value.Get("disabled").Truthy()) { + failure = @"disabled"; + } else if (appKey.length == 0) { + failure = @"invalid_config"; + } else { + config->value = value; + config->appKey = appKey; + } + } + if (reason != NULL) { + *reason = failure; + } + return failure == nil; +} + +// Host-facing result for a config that cannot run: absent or disabled is a +// skip, a malformed one a failure. +static NSDictionary *PushyUnrunnableConfigResult(NSString *reason) { + BOOL skip = [reason isEqualToString:@"not_configured"] || [reason isEqualToString:@"disabled"]; + return PushyHostResult(skip ? @"skipped" : @"failed", reason, nil, NO); +} + +@implementation RCTPushyOrchestrator + ++ (void)persistConfiguration:(NSString *)config { + PushyWithStateLock(^{ + NSUserDefaults *defaults = PushyDefaults(); + if ([defaults stringForKey:keyUuid].length == 0) { + [defaults setObject:[NSUUID UUID].UUIDString forKey:keyUuid]; + } + if (PushyConfigsEquivalent([defaults stringForKey:keyNativeConfig], config)) { + // The same config in another serialization (key order, trailing + // slashes, defaulted keys). Not a change: the stored text, the + // response cache and any in-flight round all stay valid. + return; + } + // A different request identity/policy: rounds started under the old + // one must not commit (pushyNativeConfigGeneration, checked by + // commitRoundWithGeneration next to the reset generation), and the + // cached response and JS-completed marker belong to the old one. + pushyNativeConfigGeneration.fetch_add(1); + [defaults setObject:config forKey:keyNativeConfig]; + [defaults removeObjectForKey:keyNativeCheckCache]; + [self recordJsRound:nil]; + }); + if (pushySyncReady.load() && [self hasRunnableConfig]) { + PushyInstallCrashHoldHandler(); + } +} + ++ (BOOL)hasRunnableConfig { + PushyRunnableConfig config; + return PushyLoadRunnableConfig(&config, NULL); +} + +// Process-wide round state, set up by whichever launch path needs a round +// first: the delayed cold-start check or the tvOS purged-version restore. ++ (void)prepareProcess:(NSString *)launchRolledBackVersion { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pushyRoundDone = dispatch_semaphore_create(0); + pushyHostRoundGroup = dispatch_group_create(); + dispatch_group_enter(pushyHostRoundGroup); + pushyProcessAnchorUptime = PushyMonotonicNow(); + pushyLaunchRolledBackForHold = [launchRolledBackVersion copy]; + pushySyncReady.store(true); + // The crash-hold rescue shares the orchestrator's rollout gate: no + // runnable config, no handler (§11.3). persistConfiguration installs + // it once a runnable config arrives later in the process. + if ([self hasRunnableConfig]) { + PushyInstallCrashHoldHandler(); + } + }); +} + ++ (void)scheduleFromColdStart:(NSString *)launchRolledBackVersion { +#if !DEBUG + // Once per process; a few seconds of delay keeps the check away from the + // cold-start critical path (§7 R5) — its result targets the NEXT launch. + // Unless the previous process died mid-round (residual incomplete + // marker), in which case every launch second counts (§11.4). + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + [self prepareProcess:launchRolledBackVersion]; + NSUserDefaults *defaults = PushyDefaults(); + int64_t delaySeconds = + [defaults objectForKey:keyNativeCheckIncomplete] != nil ? 0 : 5; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delaySeconds * NSEC_PER_SEC), + dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + if ([self hasJsRound]) { + // Not consuming the round: a later crash rescue may still + // need it. + NSLog(@"RCTPushy -- native sync skipped: JS already queried in this process"); + return; + } + [self startRoundWithDeadline:0]; + }); + }); +#endif +} + +#if TARGET_OS_TV && !DEBUG +// Runs this process's round before the first bundle loads, blocking the +// caller for at most what is left of kPushyPurgeRestoreBudget since process +// start. The state no longer has a +// current version, so the server answers with the latest version for the +// packaged bundle, and the round activates it while the restore window is +// open. Returns NO when no round can run; otherwise the caller must resolve +// the launch bundle again: it launches whatever was activated before the +// window closed, or the packaged bundle. The round stops at the same +// deadline, keeping a partial download for the next check to resume. ++ (BOOL)restorePurgedLaunch:(NSString *)purgedVersion + rolledBack:(NSString *)launchRolledBackVersion { + [self prepareProcess:launchRolledBackVersion]; + if (![self hasRunnableConfig]) { + RCTLogWarn(@"RCTPushy -- version %@ was purged and no native sync is " + @"configured; launching the packaged bundle", purgedVersion); + return NO; + } + // Offline: the round could only burn the launch budget. + if (!PushyHasNetworkPath()) { + RCTLogWarn(@"RCTPushy -- version %@ was purged and there is no network; " + @"launching the packaged bundle", purgedVersion); + return NO; + } + // The launch watchdog counts from process start, not from this call: + // whatever the host did before resolving the bundle is already spent. + NSTimeInterval budget = kPushyPurgeRestoreBudget - PushyProcessElapsed(); + if (budget < kPushyPurgeRestoreMinimumBudget) { + RCTLogWarn(@"RCTPushy -- version %@ was purged and the launch budget is " + @"spent; launching the packaged bundle", purgedVersion); + return NO; + } + PushyWithStateLock(^{ + pushyPurgeRestoreWindowOpen = true; + }); + pushyPurgeRestoreActive.store(true); + NSTimeInterval deadline = PushyMonotonicNow() + budget; + dispatch_semaphore_t done = dispatch_semaphore_create(0); + dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{ + [self startRoundWithDeadline:deadline]; + dispatch_semaphore_signal(done); + }); + BOOL timedOut = dispatch_semaphore_wait(done, dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(budget * NSEC_PER_SEC))) != 0; + // The round's deadline does not bound a slow response (the request + // timeout is an idle timeout) nor a version already on disk, so the close + // is what stops a late activation — not the deadline. + PushyWithStateLock(^{ + pushyPurgeRestoreWindowOpen = false; + }); + if (timedOut) { + RCTLogWarn(@"RCTPushy -- purged version %@ restore timed out", purgedVersion); + } else { + NSDictionary *result = pushyHostRoundResult; + RCTLogInfo(@"RCTPushy -- purged version %@ restore: %@ %@ %@", purgedVersion, + result[@"status"], result[@"reason"], result[@"hash"]); + } + return YES; +} +#endif + ++ (NSDictionary *)prepareBundle { + return [self prepareBundleWithin:kPushyHostPrepareDefaultTimeout]; +} + +// Waits at most `timeout` seconds for this process's round. The round itself +// is not cut short by the host's wait (its result still serves the next +// launch); the host gets failed/timeout instead of a result. ++ (NSDictionary *)prepareBundleWithin:(NSTimeInterval)timeout { +#if DEBUG + return PushyHostResult(@"skipped", @"debug", nil, NO); +#else + if (!pushySyncReady.load()) { + return PushyHostResult(@"skipped", @"not_initialized", nil, NO); + } + PushyRunnableConfig runnable; + NSString *reason = nil; + if (!PushyLoadRunnableConfig(&runnable, &reason)) { + return PushyUnrunnableConfigResult(reason); + } + NSString *configJson = runnable.json; + // Run the round on its own queue rather than this caller's thread, so the + // wait below can be bounded. + if ([self claimRound]) { + dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{ + [self runClaimedRoundWithDeadline:0]; + }); + } + // A group is broadcast-style. Sharing the rescue semaphore would let one + // waiter consume the only signal and leave the other waiting forever. + if (!pushyRoundStarted.load()) { + return PushyHostResult(@"skipped", @"config_changed", nil, NO); + } + if (!(timeout > 0)) { + timeout = 0; // also NaN + } + if (dispatch_group_wait(pushyHostRoundGroup, dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(MIN(timeout, (NSTimeInterval)(INT64_MAX / NSEC_PER_SEC)) * NSEC_PER_SEC))) != 0) { + return PushyHostResult(@"failed", @"timeout", nil, NO); + } + if (pushyHostRoundConfigGeneration != pushyNativeConfigGeneration.load() + || !PushyConfigsEquivalent(configJson, pushyHostRoundConfig) + || !PushyConfigsEquivalent(configJson, [PushyDefaults() stringForKey:keyNativeConfig])) { + return PushyHostResult(@"cancelled", @"config_changed", nil, NO); + } + if (pushyHostRoundGeneration != pushyResetGeneration.load()) { + return PushyHostResult(@"cancelled", @"reset", nil, NO); + } + return pushyHostRoundResult ?: PushyHostResult(@"failed", @"internal_error", nil, NO); +#endif +} + ++ (void)recordJsRound:(NSString *)config { + @synchronized (RCTPushyOrchestrator.class) { + pushyJsCompletedConfig = [config copy]; + } +} + +// True when JS already obtained a valid response in this process for a config +// equivalent to the one the native round would use (§10.3): the delayed round is +// then a duplicate request. Only the scheduled round asks — the crash-rescue +// path still runs, JS is dead by then. ++ (BOOL)hasJsRound { + NSString *jsConfig; + @synchronized (RCTPushyOrchestrator.class) { + jsConfig = pushyJsCompletedConfig; + } + if (jsConfig == nil) { + return NO; + } + return PushyConfigsEquivalent([PushyDefaults() stringForKey:keyNativeConfig], jsConfig); +} + +// Runs the process's single round on the calling thread if nobody has +// started it yet. deadlineUptime > 0 (crash rescue) caps every HTTP call and +// download phase to the remaining budget. ++ (void)startRoundWithDeadline:(NSTimeInterval)deadlineUptime { + if ([self claimRound]) { + [self runClaimedRoundWithDeadline:deadlineUptime]; + } +} + +// Claims this process's single round. Missing/disabled configuration is a +// preflight skip, not a used round. A YES must be followed by exactly one +// runClaimedRoundWithDeadline:. ++ (BOOL)claimRound { + if (![self hasRunnableConfig]) { + return NO; + } + bool expected = false; + return pushyRoundStarted.compare_exchange_strong(expected, true); +} + ++ (void)runClaimedRoundWithDeadline:(NSTimeInterval)deadlineUptime { + @try { + [self runOnce:pushyLaunchRolledBackForHold deadline:deadlineUptime]; + } @catch (NSException *exception) { + // The rescue path must never take the app down with it. + RCTLogWarn(@"RCTPushy -- native sync crashed: %@", exception.reason); + pushyHostRoundResult = PushyHostResult(@"failed", @"internal_error", nil, NO); + } @finally { + pushyRoundCompleted.store(true); + dispatch_semaphore_signal(pushyRoundDone); + dispatch_group_leave(pushyHostRoundGroup); + } +} + +// Crash-rescue entry (§11.3), called from the handler's worker queue while +// the uncaught-exception handler holds the dying process. Ensures this +// process's round runs to completion within the budget, then activates a +// downloaded-but-unactivated version if one exists — the last chance before +// the process is gone. ++ (void)runHoldRoundWithDeadline:(NSTimeInterval)deadlineUptime { + pushyCrashHoldActive.store(true); + [self startRoundWithDeadline:deadlineUptime]; + if (pushyRoundStarted.load() && !pushyRoundCompleted.load()) { + NSTimeInterval remaining = deadlineUptime - PushyMonotonicNow(); + if (remaining > 0) { + dispatch_semaphore_wait(pushyRoundDone, dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(remaining * NSEC_PER_SEC))); + } + } + [self activatePendingVersion]; +} + +// The alert-strategy variant of the §10.7 hole: the round downloaded a fix +// but deferred activation to JS, and JS is now dead. Activation is local and +// bounded (a state switch under the commit lock). ++ (void)activatePendingVersion { + NSString *hash = nil; + uint64_t generation = 0; + uint64_t configGeneration = 0; + @synchronized (self) { + hash = pushyUnactivatedHash; + generation = pushyUnactivatedGeneration; + configGeneration = pushyUnactivatedConfigGeneration; + } + if (hash == nil) { + return; + } + // Unlike Android's switchVersion, PushySwitchVersionLocked does not + // validate the version directory; never point the next launch at bytes + // that are gone (e.g. wiped by a reset whose generation bump we would + // catch below anyway, or by cleanup). + NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash]; + if (!PushyHasCompletedVersionAtPath(versionDir, hash)) { + NSLog(@"RCTPushy -- crash hold: version %@ no longer on disk, dropping activation", hash); + return; + } + NSDictionary *hashInfoEntry = nil; + NSString *existingInfo = [PushyDefaults() stringForKey:PushyHashInfoKey(hash)]; + NSData *existingData = [existingInfo dataUsingEncoding:NSUTF8StringEncoding]; + id parsed = existingData == nil ? nil : + [NSJSONSerialization JSONObjectWithData:existingData options:0 error:nil]; + if ([parsed isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *merged = [parsed mutableCopy]; + merged[PushyInfoCrashHold()] = @YES; + hashInfoEntry = @{@"hash": hash, @"info": merged}; + } + BOOL committed = [self commitRoundWithGeneration:generation + configGeneration:configGeneration + hashInfo:hashInfoEntry + activate:hash + responseText:nil + request:nil + config:nil + responseAt:0 + activated:NULL]; + if (committed) { + @synchronized (self) { + pushyUnactivatedHash = nil; + } + NSLog(@"RCTPushy -- crash hold: activated downloaded version %@", hash); + } else { + NSLog(@"RCTPushy -- crash hold: reset or new config since download, dropping activation"); + } +} + +// A bare module instance drives the existing download/patch pipeline without +// a bridge: the file queue is process-global and progress events are gated on +// hasListeners (never set without a bridge). ++ (RCTPushy *)engine { + static RCTPushy *engine; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + engine = [[RCTPushy alloc] init]; + }); + return engine; +} + ++ (void)runOnce:(NSString *)launchRolledBackVersion deadline:(NSTimeInterval)deadlineUptime { + // Generations first, config second: a config replaced in between is + // caught by the commit's generation check. + const uint64_t resetGeneration = pushyResetGeneration.load(); + const uint64_t configGeneration = pushyNativeConfigGeneration.load(); + pushyHostRoundGeneration = resetGeneration; + pushyHostRoundConfigGeneration = configGeneration; + pushyHostRoundResult = PushyHostResult(@"failed", @"check_failed", nil, NO); + NSUserDefaults *defaults = PushyDefaults(); + PushyRunnableConfig config; + NSString *reason = nil; + BOOL runnable = PushyLoadRunnableConfig(&config, &reason); + pushyHostRoundConfig = config.json; + if (!runnable) { + pushyHostRoundResult = PushyUnrunnableConfigResult(reason); + return; + } + // Preserve the interrupted-round breadcrumb and reset-safe atomic commit. + [defaults setObject:@YES forKey:keyNativeCheckIncomplete]; + @try { + [self runConfiguredRound:config.value + configJson:config.json + appKey:config.appKey + launchRolledBackVersion:launchRolledBackVersion + resetGeneration:resetGeneration + configGeneration:configGeneration + deadline:deadlineUptime]; + } @finally { + [defaults removeObjectForKey:keyNativeCheckIncomplete]; + } +} + ++ (void)runConfiguredRound:(const flowjson::Value &)config + configJson:(NSString *)configJson + appKey:(NSString *)appKey + launchRolledBackVersion:(NSString *)launchRolledBackVersion + resetGeneration:(uint64_t)resetGeneration + configGeneration:(uint64_t)configGeneration + deadline:(NSTimeInterval)deadlineUptime { + NSUserDefaults *defaults = PushyDefaults(); + NSString *packageVersion = + PushyFromStdString(config.Get("packageVersion").AsString()); + if (packageVersion.length == 0) { + packageVersion = [RCTPushy packageVersion]; + } + + __block NSString *currentVersion = nil; + PushyWithStateLock(^{ + pushy::state::State state = PushyStateFromDefaults(PushyDefaults()); + currentVersion = PushyFromStdString(state.current_version); + }); + NSString *rolledBackVersion = launchRolledBackVersion; + NSString *uuid = [defaults stringForKey:keyUuid] ?: @""; + + flowjson::Value identity = flowjson::Value::Object(); + identity.Set("packageVersion", + flowjson::Value::String(PushyToStdString(packageVersion))); + if (currentVersion != nil) { + identity.Set("currentVersion", + flowjson::Value::String(PushyToStdString(currentVersion))); + } + identity.Set("uuid", flowjson::Value::String(PushyToStdString(uuid))); + if (rolledBackVersion != nil) { + identity.Set("rolledBackVersion", + flowjson::Value::String(PushyToStdString(rolledBackVersion))); + } + + flowjson::Value cInfo = flowjson::Value::Object(); + cInfo.Set("rnu", config.Get("rnu")); + cInfo.Set("rn", config.Get("rn")); + cInfo.Set("os", flowjson::Value::String(PushyToStdString([NSString + stringWithFormat:@"%@ %@", PushyOsName, PushyOsVersion()]))); + cInfo.Set("uuid", flowjson::Value::String(PushyToStdString(uuid))); + + flowjson::Value input = flowjson::Value::Object(); + input.Set("packageVersion", identity.Get("packageVersion")); + if (currentVersion != nil) { + input.Set("currentVersion", identity.Get("currentVersion")); + } + input.Set("buildTime", + flowjson::Value::String(PushyToStdString([RCTPushy buildTime]))); + input.Set("cInfo", cInfo); + input.Set("supportedDiffVersion", + flowjson::Value::Number(pushy::hbc::kSupportedDiffVersion)); + // Cache-only while a crash is being held (§11.3 budget) or the launch is + // blocked on the tvOS purged-version restore; computed (and shared with JS + // getBundleHash) on the ordinary delayed round. + input.Set("bundleHash", + flowjson::Value::String(PushyToStdString(PushyBundleHashSync( + !(pushyCrashHoldActive.load() || pushyPurgeRestoreActive.load()))))); + + std::string bodyJson = + flowjson::Stringify(flowcore::BuildRequestBody(input)); + NSString *body = [NSString stringWithUTF8String:bodyJson.c_str()]; + if (body == nil) { + RCTLogWarn(@"RCTPushy -- native sync: request body is not valid UTF-8"); + pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_request", nil, NO); + return; + } + + NSString *responseText = [self runQueryRequest:config + appKey:appKey + body:body + deadline:deadlineUptime]; + if (responseText == nil) { + RCTLogInfo(@"RCTPushy -- native sync: no endpoint reachable, giving up until next launch"); + return; + } + long long responseAtSeconds = (long long)[[NSDate date] timeIntervalSince1970]; + + flowjson::Value decision = flowcore::HandleResponse( + PushyToStdString(responseText), identity, false, + config.Get("afterDownload").AsString()); + if (decision.Get("action").AsString() != "download") { + BOOL committed = [self commitRoundWithGeneration:resetGeneration + configGeneration:configGeneration + hashInfo:nil + activate:nil + responseText:responseText + request:body + config:configJson + responseAt:responseAtSeconds + activated:NULL]; + pushyHostRoundResult = committed + ? PushyHostResult(PushyStatusNone(), PushyFromStdString(decision.Get("reason").AsString()), nil, NO) + : PushyHostResult(@"cancelled", @"reset", nil, NO); + RCTLogInfo(@"RCTPushy -- native sync: nothing to do (%s)", + decision.Get("reason").AsString().c_str()); + return; + } + NSString *hash = PushyFromStdString(decision.Get("hash").AsString()); + if (!PushyIsSafePathComponent(hash)) { + pushyHostRoundResult = PushyHostResult(@"failed", @"invalid_response", nil, NO); + return; + } + + NSString *versionDir = [[RCTPushy downloadDir] stringByAppendingPathComponent:hash]; + BOOL downloaded = PushyHasCompletedVersionAtPath(versionDir, hash); + if (!downloaded) { + downloaded = [self performAttempts:decision.Get("attempts") + hash:hash + originHash:currentVersion + deadline:deadlineUptime]; + } + if (!downloaded) { + BOOL committed = [self commitRoundWithGeneration:resetGeneration + configGeneration:configGeneration + hashInfo:nil + activate:nil + responseText:responseText + request:body + config:configJson + responseAt:responseAtSeconds + activated:NULL]; + pushyHostRoundResult = committed + ? PushyHostResult(@"failed", @"download_failed", nil, NO) + : PushyHostResult(@"cancelled", @"reset", nil, NO); + return; + } + + // Persist name/description/metaInfo alongside the version, mirroring the + // JS side's setLocalHashInfo after a successful download. + const flowjson::Value &info = decision.Get("info"); + NSMutableDictionary *versionInfo = [NSMutableDictionary dictionary]; + for (const char *key : {"name", "description", "metaInfo"}) { + if (info.Get(key).IsString()) { + versionInfo[@(key)] = PushyFromStdString(info.Get(key).AsString()) ?: @""; + } + } + // A forceBoot activation is the brick-rescue path: mark it in the + // persisted info so JS can report force_boot_rescue when this version + // survives to markSuccess. Only the server-sent directive counts — a + // silent-strategy activation is ordinary delivery. + if (info.Get("config").Get("forceBoot").Truthy()) { + versionInfo[PushyInfoForceBoot()] = @YES; + } + if (pushyCrashHoldActive.load()) { + versionInfo[PushyInfoCrashHold()] = @YES; + } + // Silent strategies or a server-marked forceBoot version (per-version + // remote override — the brick rescue) activate for the next launch; + // otherwise activation stays with the JS side (§6/§10.1). Unless a crash + // is being held: JS is dead, deferring to it would leave the fix on disk + // forever (§11.3). Or the launch is blocked on reinstalling a version + // tvOS purged: JS has not started, and the whole point is to boot it — + // commitRoundWithGeneration drops that one once the restore window closed. + BOOL activate = decision.Get("activate").Truthy() || pushyCrashHoldActive.load() + || pushyPurgeRestoreActive.load(); + BOOL activated = NO; + BOOL committed = [self commitRoundWithGeneration:resetGeneration + configGeneration:configGeneration + hashInfo:@{@"hash": hash, @"info": versionInfo} + activate:activate ? hash : nil + responseText:responseText + request:body + config:configJson + responseAt:responseAtSeconds + activated:&activated]; + activate = activated; + if (!committed) { + RCTLogInfo(@"RCTPushy -- native sync: reset during round, dropping result"); + } else if (activate) { + @synchronized (self) { + pushyUnactivatedHash = nil; + } + RCTLogInfo(@"RCTPushy -- native sync: downloaded %@ and set for next launch", hash); + } else { + // Remembered so a crash later in this process can still activate it + // (activatePendingVersion) — JS never will. + @synchronized (self) { + pushyUnactivatedHash = [hash copy]; + pushyUnactivatedGeneration = resetGeneration; + pushyUnactivatedConfigGeneration = configGeneration; + } + RCTLogInfo(@"RCTPushy -- native sync: downloaded %@, activation left to JS", hash); + } + pushyHostRoundResult = committed + ? PushyHostResult(@"downloaded", @"", hash, activate) + : PushyHostResult(@"cancelled", @"reset", nil, NO); +} + +// Everything a round persists — version info, the activation, the response +// cache — is written inside ONE state-lock acquisition that first re-checks the +// reset and config generations. resetToPackagedBundle bumps that generation under the same +// lock, so there is no compare-and-act window: either the whole round commits, +// or the reset wins and none of it does. ++ (BOOL)commitRoundWithGeneration:(uint64_t)generation + configGeneration:(uint64_t)configGeneration + hashInfo:(NSDictionary *)hashInfoEntry + activate:(NSString *)hashToActivate + responseText:(NSString *)responseText + request:(NSString *)requestBody + config:(NSString *)configJson + responseAt:(long long)responseAtSeconds + activated:(BOOL *)activatedOut { + // responseText is nil for the crash handler's activation-only commit + // (activatePendingVersion): no round ran, so there is no cache to write. + NSData *cacheData = nil; + if (responseText != nil) { + NSDictionary *cacheEntry = @{ + @"ts": @(responseAtSeconds), + @"body": responseText, + @"request": requestBody, + @"config": configJson, + }; + cacheData = [NSJSONSerialization dataWithJSONObject:cacheEntry options:0 error:nil]; + } + __block BOOL committed = NO; + __block BOOL activated = NO; + PushyWithStateLock(^{ + if (pushyResetGeneration.load() != generation) { + return; + } + // The config the round ran with was replaced (persistConfiguration + // bumps this under the same lock). + if (pushyNativeConfigGeneration.load() != configGeneration) { + return; + } + NSString *activation = hashToActivate; + NSDictionary *info = hashInfoEntry[@"info"]; + // The tvOS purged-version restore round (a round commit, not the + // crash handler's activation-only one): it may take over this launch + // only while +bundleURL still waits for it; after that the version is + // left to JS unless a crash is being held (JS is dead then). + if (responseText != nil && pushyPurgeRestoreActive.load()) { + if (pushyPurgeRestoreWindowOpen) { + if (activation != nil && info != nil) { + NSMutableDictionary *marked = [info mutableCopy]; + marked[@"purgeRestore"] = @YES; + info = marked; + } + } else if (!pushyCrashHoldActive.load()) { + activation = nil; + } + } + NSUserDefaults *defaults = PushyDefaults(); + if (hashInfoEntry != nil) { + NSData *infoData = [NSJSONSerialization dataWithJSONObject:info + options:0 + error:nil]; + if (infoData != nil) { + [defaults setObject:[[NSString alloc] initWithData:infoData encoding:NSUTF8StringEncoding] + forKey:PushyHashInfoKey(hashInfoEntry[@"hash"])]; + } + } + if (activation != nil) { + PushySwitchVersionLocked(activation); + activated = YES; + } + if (cacheData != nil) { + [defaults setObject:[[NSString alloc] initWithData:cacheData encoding:NSUTF8StringEncoding] + forKey:keyNativeCheckCache]; + } + committed = YES; + }); + if (activatedOut != NULL) { + *activatedOut = activated; + } + return committed; +} + +// Sequential fallback over the ordered candidates (§5.1): one request at a +// time with its own timeout; after the configured round fails, queryUrls +// discovery merges remote candidates (excluding the already-tried) for one +// more round. No hedged race on purpose — this path is latency-insensitive. +// Per-request timeout, capped to the crash-rescue budget when one is active. +// <= 0 means the budget is gone and the round must stop issuing requests. +static NSTimeInterval PushyQueryRequestTimeout(NSTimeInterval deadlineUptime) { + if (deadlineUptime <= 0) { + return 10; + } + return MIN(10, deadlineUptime - PushyMonotonicNow()); +} + ++ (NSString *)runQueryRequest:(const flowjson::Value &)config + appKey:(NSString *)appKey + body:(NSString *)body + deadline:(NSTimeInterval)deadlineUptime { + NSMutableSet *tried = [NSMutableSet set]; + const NSUInteger maxHttpAttempts = 8; + __block NSUInteger httpAttempts = 0; + // Spends one attempt of the shared budget. NO once the attempt cap or the + // deadline is exhausted (the round must stop issuing requests); the + // per-request timeout lands in *timeout otherwise. + BOOL (^takeAttempt)(NSTimeInterval *) = ^BOOL(NSTimeInterval *timeout) { + if (httpAttempts++ >= maxHttpAttempts) { + return NO; + } + *timeout = PushyQueryRequestTimeout(deadlineUptime); + return *timeout > 0; + }; + // Tries each not-yet-tried endpoint base in order and returns the first + // valid response; *stop is set when the budget ran out. + NSString *(^queryEndpoints)(const flowjson::Value &, BOOL *) = + ^NSString *(const flowjson::Value &endpoints, BOOL *stop) { + for (const auto &endpoint : endpoints.elements()) { + NSString *base = PushyNormalizeEndpointBase( + PushyFromStdString(endpoint.AsString())); + if (base.length == 0 || [tried containsObject:base]) { + continue; + } + NSTimeInterval timeout = 0; + if (!takeAttempt(&timeout)) { + *stop = YES; + return nil; + } + [tried addObject:base]; + NSString *response = PushyHttpRequest( + [NSString stringWithFormat:@"%@%@%@", base, RCTPushyQueryPath(), appKey], + @"POST", body, timeout); + if (PushyIsValidResponse(response)) { + return response; + } + } + return nil; + }; + + BOOL stop = NO; + double sample = arc4random() / 4294967296.0; + NSString *response = queryEndpoints( + flowcore::OrderEndpointCandidates(config.Get("endpoints"), sample), &stop); + if (response != nil || stop) { + return response; + } + for (const auto &queryUrl : config.Get("queryUrls").elements()) { + NSString *listUrl = PushyFromStdString(queryUrl.AsString()); + if (listUrl == nil) { + continue; + } + NSTimeInterval listTimeout = 0; + if (!takeAttempt(&listTimeout)) { + return nil; + } + NSString *listText = PushyHttpRequest(listUrl, @"GET", nil, listTimeout); + if (listText == nil) { + continue; + } + bool ok = false; + flowjson::Value remote = flowjson::Parse(PushyToStdString(listText), &ok); + if (!ok || !remote.IsArray()) { + continue; + } + // One successfully fetched remote list is enough. + return queryEndpoints(remote, &stop); + } + return nil; +} + ++ (BOOL)performAttempts:(const flowjson::Value &)attempts + hash:(NSString *)hash + originHash:(NSString *)originHash + deadline:(NSTimeInterval)holdDeadline { + RCTPushy *engine = [self engine]; + // Crash-rescue budget caps every phase; 0 keeps the normal 10min windows. + NSTimeInterval incrementalDeadline = PushyMonotonicNow() + 600; + if (holdDeadline > 0) { + incrementalDeadline = MIN(incrementalDeadline, holdDeadline); + } + NSTimeInterval fullDeadline = 0; + for (const auto &attempt : attempts.elements()) { + const std::string &type = attempt.Get("type").AsString(); + PushyType pushyType = type == "diff" ? PushyTypePatchFromPpk + : type == "pdiff" ? PushyTypePatchFromPackage + : PushyTypeFullDownload; + if (pushyType == PushyTypePatchFromPpk && originHash.length == 0) { + continue; // diff patches from the running version; none running + } + BOOL isFullAttempt = pushyType == PushyTypeFullDownload; + if (isFullAttempt && fullDeadline == 0) { + // diff/pdiff cannot starve the last-resort full download. + fullDeadline = PushyMonotonicNow() + 600; + if (holdDeadline > 0) { + fullDeadline = MIN(fullDeadline, holdDeadline); + } + } + NSTimeInterval deadline = isFullAttempt ? fullDeadline : incrementalDeadline; + for (const auto &urlValue : attempt.Get("urls").elements()) { + NSString *url = PushyFromStdString(urlValue.AsString()); + if (url == nil) { + continue; + } + NSTimeInterval remaining = deadline - PushyMonotonicNow(); + if (remaining <= 0) { + if (isFullAttempt) { + return NO; + } + break; + } + NSMutableDictionary *options = + [@{ + PushyOptionUrlKey(): url, + @"hash": hash, + @"deadlineUptime": @(deadline), + } mutableCopy]; + if (pushyType == PushyTypePatchFromPpk) { + options[@"originHash"] = originHash; + } + dispatch_semaphore_t sem = dispatch_semaphore_create(0); + __block NSError *resultError = nil; + [engine performFetch:pushyType options:options callback:^(NSError *error) { + resultError = error; + dispatch_semaphore_signal(sem); + }]; + if (dispatch_semaphore_wait(sem, dispatch_time(DISPATCH_TIME_NOW, + (int64_t)(remaining * NSEC_PER_SEC))) != 0) { + RCTLogWarn(@"RCTPushy -- native sync: %s attempt timed out", type.c_str()); + if (isFullAttempt) { + return NO; + } + break; + } + if (resultError == nil) { + return YES; + } + RCTLogInfo(@"RCTPushy -- native sync: %s attempt failed: %@", + type.c_str(), resultError.localizedDescription); + } + } + return NO; +} + +@end diff --git a/react-native-update.podspec b/react-native-update.podspec index 0ecc4fea..225b61b9 100644 --- a/react-native-update.podspec +++ b/react-native-update.podspec @@ -212,7 +212,13 @@ Pod::Spec.new do |s| s.subspec 'RCTPushy' do |ss| ss.dependency 'react-native-update/Core' ss.source_files = ['ios/RCTPushy/*.{h,m,mm}'] - ss.public_header_files = ['ios/RCTPushy/*.h'] + ss.public_header_files = ['ios/RCTPushy/RCTPushy.h', + 'ios/RCTPushy/RCTPushyConfiguration.h', + 'ios/RCTPushy/RCTPushyDownloader.h'] + # RCTPushyInternal.h is shared between RCTPushy.mm and + # RCTPushyOrchestrator.mm only; it pulls in C++ headers, so it must stay + # out of the module umbrella. + ss.private_header_files = ['ios/RCTPushy/RCTPushyInternal.h'] end # Conditionally add Expo subspec and check ExpoModulesCore version diff --git a/scripts/test-ios-purge-restore.sh b/scripts/test-ios-purge-restore.sh index 1ced56d9..fb476abc 100755 --- a/scripts/test-ios-purge-restore.sh +++ b/scripts/test-ios-purge-restore.sh @@ -26,9 +26,9 @@ build() { destination="$BUILD_DIR/$variant" mkdir -p "$destination" if [ "$variant" = baseline ]; then - python3 "$TEST_DIR/extract.py" "$ROOT_DIR/ios/RCTPushy/RCTPushy.mm" "$destination" + python3 "$TEST_DIR/extract.py" "$ROOT_DIR/ios/RCTPushy" "$destination" else - python3 "$TEST_DIR/extract.py" "$ROOT_DIR/ios/RCTPushy/RCTPushy.mm" "$destination" --mutation "$variant" + python3 "$TEST_DIR/extract.py" "$ROOT_DIR/ios/RCTPushy" "$destination" --mutation "$variant" fi # Compile the real production bodies and state_core; only their external I/O # collaborators live in the test host. No RN, CocoaPods, simulator or network. diff --git a/scripts/tests/ios-purge-restore/README.md b/scripts/tests/ios-purge-restore/README.md index 7bdb5928..255b6696 100644 --- a/scripts/tests/ios-purge-restore/README.md +++ b/scripts/tests/ios-purge-restore/README.md @@ -1,7 +1,8 @@ # Deterministic native purge-restore ordering tests Follow-up to #646. These tests exercise the tvOS Release branch of the real -`RCTPushy.mm` startup/commit/reset code in a macOS Foundation-only executable. +`RCTPushy.mm` / `RCTPushyOrchestrator.mm` startup/commit/reset code in a macOS +Foundation-only executable. There are no production code changes, React Native mocks to install, CocoaPods, simulator builds, network requests, or sleeps. @@ -25,8 +26,10 @@ python3 -m unittest discover -s scripts/tests/ios-purge-restore -p test_extract. ## Production code, not a second state machine -`extract.py` reads the checkout's `ios/RCTPushy/RCTPushy.mm` on every build. It -copies the exact definitions (with `#line` locations) of `bundleURL`, +`extract.py` reads the checkout's `ios/RCTPushy/RCTPushy.mm` and +`RCTPushyOrchestrator.mm` on every build (each definition must exist exactly +once across them; pod-internal `PUSHY_HIDDEN` definitions count like `static` +ones). It copies the exact definitions (with `#line` locations) of `bundleURL`, `resolveLaunchBundleURL`, `restorePurgedLaunch`, `commitRoundWithGeneration`, `resetToPackagedBundle`, their state/defaults helpers, and the relevant globals into build-only `.inc` files. These are compiled unchanged with the actual diff --git a/scripts/tests/ios-purge-restore/extract.py b/scripts/tests/ios-purge-restore/extract.py index c212bde7..5d4af402 100644 --- a/scripts/tests/ios-purge-restore/extract.py +++ b/scripts/tests/ios-purge-restore/extract.py @@ -46,8 +46,13 @@ def definition(source: str, pattern: str, label: str) -> tuple[int, str]: return definitions[0] +# File-local (static) or shared within the pod (PUSHY_HIDDEN, see +# RCTPushyInternal.h); the test host compiles everything in one unit. +LINKAGE = r'(?:static|PUSHY_HIDDEN)' + + def function(source: str, name: str) -> tuple[int, str]: - return definition(source, rf'^static\b[^\n;]*?\b{re.escape(name)}\s*\(', name) + return definition(source, rf'^{LINKAGE}\b[^\n;]*?\b{re.escape(name)}\s*\(', name) def method(source: str, name: str) -> tuple[int, str]: @@ -59,7 +64,7 @@ def exported_method(source: str, name: str) -> tuple[int, str]: def declaration(source: str, name: str) -> tuple[int, str]: - matches = list(re.finditer(rf'^static\b[^\n;]*\b{re.escape(name)}\b[^\n;]*;', + matches = list(re.finditer(rf'^{LINKAGE}\b[^\n;]*\b{re.escape(name)}\b[^\n;]*;', mask_literals(source), re.MULTILINE)) if len(matches) != 1: raise ValueError(f'{name}: expected one static declaration, found {len(matches)}') @@ -106,37 +111,60 @@ def mutate(text: str, mutation: str) -> str: return mutated -def generate(source_path: Path, output: Path, mutation: str | None = None) -> None: - source = source_path.read_text(encoding='utf-8') - restore = method(source, 'restorePurgedLaunch') - commit = method(source, 'commitRoundWithGeneration') +# The sources the module is split across (RCTPushy.mm: module and launch +# resolution; RCTPushyOrchestrator.mm: the native round). +SOURCES = ('RCTPushy.mm', 'RCTPushyOrchestrator.mm') + + +def locate(sources: list[tuple[Path, str]], extractor, name: str) -> tuple[Path, int, str]: + """Exactly one definition across all sources; each keeps its own file.""" + found = [] + for path, text in sources: + try: + line, snippet = extractor(text, name) + except ValueError as error: + if 'found 0' in str(error): + continue + raise + found.append((path, line, snippet)) + if len(found) != 1: + raise ValueError(f'{name}: expected one definition across sources, found {len(found)}') + return found[0] + + +def generate(source_dir: Path, output: Path, mutation: str | None = None) -> None: + sources = [(source_dir / name, (source_dir / name).read_text(encoding='utf-8')) + for name in SOURCES] + restore = locate(sources, method, 'restorePurgedLaunch') + commit = locate(sources, method, 'commitRoundWithGeneration') if mutation == 'skip-reresolve': - restore = (restore[0], mutate(restore[1], mutation)) + restore = (restore[0], restore[1], mutate(restore[2], mutation)) elif mutation: - commit = (commit[0], mutate(commit[1], mutation)) + commit = (commit[0], commit[1], mutate(commit[2], mutation)) sections = { - 'globals.inc': [declaration(source, name) for name in GLOBALS], - 'helpers.inc': [function(source, name) for name in HELPERS], - 'pushy.inc': [method(source, 'bundleURL'), method(source, 'resolveLaunchBundleURL'), - exported_method(source, 'resetToPackagedBundle')], + 'globals.inc': [locate(sources, declaration, name) for name in GLOBALS], + 'helpers.inc': [locate(sources, function, name) for name in HELPERS], + 'pushy.inc': [locate(sources, method, 'bundleURL'), + locate(sources, method, 'resolveLaunchBundleURL'), + locate(sources, exported_method, 'resetToPackagedBundle')], 'orchestrator.inc': [restore, commit], } output.mkdir(parents=True, exist_ok=True) - filename = json.dumps(str(source_path.resolve())) for name, snippets in sections.items(): output.joinpath(name).write_text(''.join( - f'#line {line} {filename}\n{text}\n\n' for line, text in snippets + f'#line {line} {json.dumps(str(path.resolve()))}\n{text}\n\n' + for path, line, text in snippets ), encoding='utf-8') def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('source', type=Path) + parser.add_argument('source_dir', type=Path, help='ios/RCTPushy') parser.add_argument('output', type=Path) parser.add_argument('--mutation', choices=MUTATIONS) args = parser.parse_args() try: - generate(args.source, args.output, args.mutation) + generate(args.source_dir, args.output, args.mutation) except (OSError, ValueError) as error: parser.exit(1, f'Native test extraction failed: {error}\n') diff --git a/scripts/tests/ios-purge-restore/purge_restore_test.mm b/scripts/tests/ios-purge-restore/purge_restore_test.mm index f404fa3e..d1971a4a 100644 --- a/scripts/tests/ios-purge-restore/purge_restore_test.mm +++ b/scripts/tests/ios-purge-restore/purge_restore_test.mm @@ -27,6 +27,9 @@ static void TestLog(NSString *, ...) {} #define RCTLogWarn TestLog #define RCTLogError TestLog +// Pod-internal linkage (RCTPushyInternal.h); one translation unit here. +#define PUSHY_HIDDEN + #include "globals.inc" static const char *testName = "setup"; From 1c97ffc88f7e135154140ab1f5270b591463c60f Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:29:55 +0800 Subject: [PATCH 15/21] chore(ios): use error_codes.h for NATIVE_CONFIG_CONFLICT; guard the orchestrator in the packlist Co-Authored-By: Claude Opus 5.5 --- ios/RCTPushy/RCTPushy.mm | 6 ++---- scripts/check-packlist.js | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index 41408dac..4f614251 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -372,10 +372,8 @@ - (void)zipArchiveDidUnzipFileAtIndex:(NSInteger)fileIndex static std::atomic ignoreRollback{false}; // Set once the host calls +configure:completion: in this process; JS -// syncNativeConfig then rejects with NATIVE_CONFIG_CONFLICT. Local literal -// until cpp/patch_core/error_codes.h carries the code (reconciled at merge). +// syncNativeConfig then rejects with NATIVE_CONFIG_CONFLICT. static std::atomic pushyHostConfigured{false}; -static const char *const kPushyNativeConfigConflict = "NATIVE_CONFIG_CONFLICT"; // Whether the host asked +bundleURL for the bundle to load (the // isUsingBundleUrl constant, mirroring Android/Harmony). NO means the app // runs a bundle this SDK never resolved, so updates cannot take effect. @@ -1159,7 +1157,7 @@ - (instancetype)init if (pushyHostConfigured.load()) { // Two owners of one config would overwrite each other on every // launch; fail loudly instead of picking one. - PushyRejectError(reject, PushyErrorWithCode(kPushyNativeConfigConflict, + PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kNativeConfigConflict, @"[NATIVE_CONFIG_CONFLICT] " @"The host app called RCTPushy configure natively in this process; " @"set nativeConfigSource: 'native' in the JS client options.")); diff --git a/scripts/check-packlist.js b/scripts/check-packlist.js index de32a01b..0933528e 100644 --- a/scripts/check-packlist.js +++ b/scripts/check-packlist.js @@ -50,6 +50,7 @@ const required = [ 'android/src/main/java/cn/reactnative/modules/update/UpdateContext.java', 'android/jni/hpatch.c', 'ios/RCTPushy/RCTPushy.mm', + 'ios/RCTPushy/RCTPushyOrchestrator.mm', 'react-native-update.podspec', 'cpp/patch_core/patch_core.cpp', 'cpp/patch_core/archive_limits.h', From 3636cec7dde50ec765d4f8f673c13b41d877ed6a Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:42:33 +0800 Subject: [PATCH 16/21] docs: state that host configure() requires nativeConfigSource: 'native' Co-Authored-By: Claude Opus 5.5 --- docs/NATIVE_HOST_API_MIGRATION.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/docs/NATIVE_HOST_API_MIGRATION.md b/docs/NATIVE_HOST_API_MIGRATION.md index 3b79d6a2..5cb68d9f 100644 --- a/docs/NATIVE_HOST_API_MIGRATION.md +++ b/docs/NATIVE_HOST_API_MIGRATION.md @@ -31,7 +31,13 @@ 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, +according to the existing `afterDownload` policy. + +**Config ownership.** Once the host calls `configure` in a process, the host owns +the configuration for that process. The JavaScript client must then be created +with `nativeConfigSource: 'native'`; in the default `'javascript'` mode its config +write is rejected with `NATIVE_CONFIG_CONFLICT` (reported through `onError` and +`console.error`). Apps that never call `configure` are unaffected. Callback threading, cancellation, request deduplication, configuration invalidation and rescue behavior are unchanged. Result fields (`status`, `reason`, `hash`, `activated`) and existing status strings, @@ -60,6 +66,10 @@ the API names that existed in the versions they document. 这是原生源码接口的破坏性重命名,不改变下载、下次启动选包、回滚或救援行为。 配置完成后,先走宿主正常的 bundle 解析流程,再调用 `prepareBundle`; 不要为调用接口再次解析 bundle,也不要新建另一个 Harmony provider。 +宿主一旦调用 `configure`,本进程的配置即归宿主所有:JS 端必须设置 +`nativeConfigSource: 'native'`,否则默认 `'javascript'` 模式下的配置写入会以 +`NATIVE_CONFIG_CONFLICT` 被拒绝(经 `onError` 与 `console.error` 报出)。 +从不调用 `configure` 的应用不受影响。 `activated` 仍表示已为下次启动选定,而不是当前实例已经重载。 JS SDK、JS 原生桥接接口、服务端路径、配置值、结果字段/状态值和持久化键不变。 From 0d3a6a901790b542d613ac06c4cf0fde8fa10b5d Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:45:01 +0800 Subject: [PATCH 17/21] revert: keep the documented native-then-JS config handover Host configure() followed by a JS takeover in the default 'javascript' mode is a documented, supported flow (last completed write wins). Rejecting it with NATIVE_CONFIG_CONFLICT would break existing integrations. The underlying harm is already gone: equivalent configs (C1) no longer count as a change, so they neither drop the response cache nor cancel in-flight rounds. Co-Authored-By: Claude Opus 5.5 --- .../modules/update/ErrorCodes.java | 1 - .../modules/update/PushyRuntime.java | 14 +----- .../modules/update/UpdateModuleImpl.java | 10 ----- cpp/patch_core/error_codes.h | 4 -- docs/NATIVE_HOST_API_MIGRATION.md | 20 +++++---- harmony/pushy/src/main/ets/ErrorCodes.ts | 2 - .../main/ets/PushyFileJSBundleProvider.ets | 2 +- .../pushy/src/main/ets/PushyTurboModule.ts | 9 ---- harmony/pushy/src/main/ets/UpdateContext.ts | 14 ------ ios/RCTPushy/RCTPushy.mm | 16 ------- src/__tests__/client.test.ts | 44 ------------------- src/client.ts | 28 +----------- src/error.ts | 6 +-- src/locales/en.ts | 2 - src/locales/zh.ts | 2 - src/type.ts | 3 -- 16 files changed, 15 insertions(+), 162 deletions(-) diff --git a/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java b/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java index 94547493..74f70916 100644 --- a/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java +++ b/android/src/main/java/cn/reactnative/modules/update/ErrorCodes.java @@ -22,7 +22,6 @@ final class ErrorCodes { static final String UNSUPPORTED_PLATFORM = "UNSUPPORTED_PLATFORM"; static final String APK_INSTALL_PERMISSION_REQUIRED = "APK_INSTALL_PERMISSION_REQUIRED"; static final String APK_INSTALL_FAILED = "APK_INSTALL_FAILED"; - static final String NATIVE_CONFIG_CONFLICT = "NATIVE_CONFIG_CONFLICT"; private ErrorCodes() { } diff --git a/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java index e7e92178..df9d5e84 100644 --- a/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +++ b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java @@ -43,27 +43,15 @@ public Thread newThread(Runnable runnable) { } }); - // Set once the host called configure() in this process. JS config writes - // (syncNativeConfig) are then rejected with NATIVE_CONFIG_CONFLICT. - private static volatile boolean hostConfigured; - - static boolean isHostConfigured() { - return hostConfigured; - } - /** * 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/prepareBundle. The host - * then owns the configuration for this process: the JS client must use - * nativeConfigSource: 'native', a JS config write is rejected with - * NATIVE_CONFIG_CONFLICT. + * Await the callback before continuing startup/prepareBundle. */ public static void configure(Context context, JSONObject options, final ConfigurationCallback callback) { if (context == null || options == null || callback == null) { throw new IllegalArgumentException("context, options and callback are required"); } - hostConfigured = true; final Context applicationContext = context.getApplicationContext(); // Snapshot caller-owned JSON before dispatch, not minutes later on a worker. final String snapshot = options.toString(); diff --git a/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java b/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java index 0c09a282..f57c6615 100644 --- a/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java +++ b/android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java @@ -335,16 +335,6 @@ public void syncNativeConfig(final String config, final Promise promise) { if (requireNonEmpty(config, "config", promise) == null) { return; } - if (PushyRuntime.isHostConfigured()) { - // The host owns the configuration in this process: a JS write - // would silently replace it. Bad integration fails loudly. - promise.reject(ErrorCodes.NATIVE_CONFIG_CONFLICT, - "[" + ErrorCodes.NATIVE_CONFIG_CONFLICT + "] the host app configured " - + "native sync with PushyRuntime.configure(); set " - + "nativeConfigSource: 'native' in the JS client instead of " - + "syncing a JS config"); - return; - } try { new JSONObject(config); } catch (JSONException e) { diff --git a/cpp/patch_core/error_codes.h b/cpp/patch_core/error_codes.h index 430ee561..852338f5 100644 --- a/cpp/patch_core/error_codes.h +++ b/cpp/patch_core/error_codes.h @@ -50,10 +50,6 @@ constexpr const char* kApkInstallPermissionRequired = "APK_INSTALL_PERMISSION_REQUIRED"; // PackageInstaller could not create, write, commit, or continue an APK session. constexpr const char* kApkInstallFailed = "APK_INSTALL_FAILED"; -// JS tried to write the native-check configuration (syncNativeConfig) in a -// process whose host already configured it natively; JS must declare -// nativeConfigSource: 'native' instead of competing for the config. -constexpr const char* kNativeConfigConflict = "NATIVE_CONFIG_CONFLICT"; } // namespace error_codes } // namespace pushy diff --git a/docs/NATIVE_HOST_API_MIGRATION.md b/docs/NATIVE_HOST_API_MIGRATION.md index 5cb68d9f..473162b5 100644 --- a/docs/NATIVE_HOST_API_MIGRATION.md +++ b/docs/NATIVE_HOST_API_MIGRATION.md @@ -33,11 +33,13 @@ reload the running React Native instance or display UI. A successful selection i for the **next launch**; preparation can also download without selecting a bundle, according to the existing `afterDownload` policy. -**Config ownership.** Once the host calls `configure` in a process, the host owns -the configuration for that process. The JavaScript client must then be created -with `nativeConfigSource: 'native'`; in the default `'javascript'` mode its config -write is rejected with `NATIVE_CONFIG_CONFLICT` (reported through `onError` and -`console.error`). Apps that never call `configure` are unaffected. Callback threading, cancellation, +**Config ownership.** With `nativeConfigSource: 'native'` the host keeps the +configuration and JS never writes it. In the default `'javascript'` mode the host +may provision the first launch with `configure` and JS takes over once it runs; the +last completed write wins. A write that is equivalent to the stored configuration +(same values after defaults are filled in, endpoint trailing `/` removed and +duplicates dropped, key order ignored) is not a change: it neither invalidates the +response cache nor cancels an in-flight round. Callback threading, cancellation, request deduplication, configuration invalidation and rescue behavior are unchanged. Result fields (`status`, `reason`, `hash`, `activated`) and existing status strings, @@ -66,10 +68,10 @@ the API names that existed in the versions they document. 这是原生源码接口的破坏性重命名,不改变下载、下次启动选包、回滚或救援行为。 配置完成后,先走宿主正常的 bundle 解析流程,再调用 `prepareBundle`; 不要为调用接口再次解析 bundle,也不要新建另一个 Harmony provider。 -宿主一旦调用 `configure`,本进程的配置即归宿主所有:JS 端必须设置 -`nativeConfigSource: 'native'`,否则默认 `'javascript'` 模式下的配置写入会以 -`NATIVE_CONFIG_CONFLICT` 被拒绝(经 `onError` 与 `console.error` 报出)。 -从不调用 `configure` 的应用不受影响。 +`nativeConfigSource: 'native'` 时配置归宿主,JS 不写入;默认 `'javascript'` +模式下可由原生 `configure` 提供首启配置、JS 运行后接管,以最后完成的写入为准。 +与已存配置等价的写入(补齐默认值、去掉服务地址末尾 `/` 并去重、忽略键序后相同) +不算变更,不会清空响应缓存,也不会作废进行中的轮次。 `activated` 仍表示已为下次启动选定,而不是当前实例已经重载。 JS SDK、JS 原生桥接接口、服务端路径、配置值、结果字段/状态值和持久化键不变。 diff --git a/harmony/pushy/src/main/ets/ErrorCodes.ts b/harmony/pushy/src/main/ets/ErrorCodes.ts index bb989e08..c03f1f58 100644 --- a/harmony/pushy/src/main/ets/ErrorCodes.ts +++ b/harmony/pushy/src/main/ets/ErrorCodes.ts @@ -22,8 +22,6 @@ export const ERROR_UNSUPPORTED_PLATFORM = 'UNSUPPORTED_PLATFORM'; export const ERROR_APK_INSTALL_PERMISSION_REQUIRED = 'APK_INSTALL_PERMISSION_REQUIRED'; export const ERROR_APK_INSTALL_FAILED = 'APK_INSTALL_FAILED'; -// JS syncNativeConfig 撞上宿主原生 configure()。 -export const ERROR_NATIVE_CONFIG_CONFLICT = 'NATIVE_CONFIG_CONFLICT'; /** * 带稳定错误码的 Error:`code` 是属性,消息同时带 `[CODE] ` 前缀(桥丢属性时 diff --git a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets index 2e731c3e..daf03618 100644 --- a/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets +++ b/harmony/pushy/src/main/ets/PushyFileJSBundleProvider.ets @@ -50,7 +50,7 @@ export class PushyFileJSBundleProvider extends JSBundleProvider { /** Configure before normal bundle resolution; no JS or network work is required. */ async configure(options: PushyConfiguration): Promise { const config = normalizePushyConfiguration(options); - await this.updateContext.setHostNativeConfig(config); + await this.updateContext.setNativeConfig(config); } /** Call after the host's real bundle resolution; never resolves it again. */ diff --git a/harmony/pushy/src/main/ets/PushyTurboModule.ts b/harmony/pushy/src/main/ets/PushyTurboModule.ts index c17f10a6..ad73d032 100644 --- a/harmony/pushy/src/main/ets/PushyTurboModule.ts +++ b/harmony/pushy/src/main/ets/PushyTurboModule.ts @@ -22,7 +22,6 @@ import { ERROR_RESTART_FAILED, ERROR_SWITCH_VERSION_FAILED, ERROR_UNSUPPORTED_PLATFORM, - ERROR_NATIVE_CONFIG_CONFLICT, createPushyError, getErrorMessage, toPushyError, @@ -264,16 +263,8 @@ export class PushyTurboModule extends UITurboModule { // read by the orchestrator; absent config = check disabled. Validated at // write time — a corrupt config would otherwise silently disable the // native check forever with no signal. - // A host that configured natively in this process owns the configuration: - // reject loudly (C2) instead of overwriting or silently ignoring it. async syncNativeConfig(config: string): Promise { logger.debug(TAG, ',call syncNativeConfig'); - if (this.context.isHostConfigured()) { - throw createPushyError( - ERROR_NATIVE_CONFIG_CONFLICT, - "host configured natively; set nativeConfigSource: 'native' in JS", - ); - } try { JSON.parse(config); } catch (e) { diff --git a/harmony/pushy/src/main/ets/UpdateContext.ts b/harmony/pushy/src/main/ets/UpdateContext.ts index 1d0ef6a8..c0e6027e 100644 --- a/harmony/pushy/src/main/ets/UpdateContext.ts +++ b/harmony/pushy/src/main/ets/UpdateContext.ts @@ -545,25 +545,11 @@ export class UpdateContext { } private static nativeConfigGeneration: number = 0; - // 宿主本进程调用过 PushyFileJSBundleProvider.configure():配置归宿主所有, - // JS 的 syncNativeConfig 必须拒绝(C2 NATIVE_CONFIG_CONFLICT),而不是 - // 悄悄覆盖或忽略——接入方式不一致要大声失败。 - private static hostConfigured: boolean = false; public getNativeConfigGeneration(): number { return UpdateContext.nativeConfigGeneration; } - public isHostConfigured(): boolean { - return UpdateContext.hostConfigured; - } - - /** 宿主原生 configure() 的写入口:先标记归属,再走共用的写入。 */ - public setHostNativeConfig(config: string): Promise { - UpdateContext.hostConfigured = true; - return this.setNativeConfig(config); - } - /** 与持久化的配置是否等价(C1)。 */ public isStoredNativeConfig(config: string | undefined): boolean { return nativeConfigsEquivalent(config, this.getKv(KEY_CONFIG)); diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index 4f614251..3245e2cf 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -371,9 +371,6 @@ - (void)zipArchiveDidUnzipFileAtIndex:(NSInteger)fileIndex static std::atomic ignoreRollback{false}; -// Set once the host calls +configure:completion: in this process; JS -// syncNativeConfig then rejects with NATIVE_CONFIG_CONFLICT. -static std::atomic pushyHostConfigured{false}; // Whether the host asked +bundleURL for the bundle to load (the // isUsingBundleUrl constant, mirroring Android/Harmony). NO means the app // runs a bundle this SDK never resolved, so updates cannot take effect. @@ -1015,10 +1012,6 @@ + (NSString *) rollback { + (void)configure:(NSDictionary *)options completion:(RCTPushyConfigurationCompletion)completion { - // From here on the host owns the config; syncNativeConfig refuses to - // overwrite it (set even when these options fail validation: the host's - // intent is what conflicts with a JS-owned config). - pushyHostConfigured.store(true); NSError *validationError = nil; // Snapshot nested mutable caller values before crossing a queue boundary. NSString *config = RCTPushyNormalizeConfiguration(options, &validationError); @@ -1154,15 +1147,6 @@ - (instancetype)init // Provisioning for the native cold-start check (NATIVE_CHECKUPDATE_DESIGN // §10.1). Validate at write time: a corrupt config would otherwise // silently disable the native check forever with no signal. - if (pushyHostConfigured.load()) { - // Two owners of one config would overwrite each other on every - // launch; fail loudly instead of picking one. - PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kNativeConfigConflict, - @"[NATIVE_CONFIG_CONFLICT] " - @"The host app called RCTPushy configure natively in this process; " - @"set nativeConfigSource: 'native' in the JS client options.")); - return; - } if (PushyStringIsBlank(config)) { PushyRejectError(reject, PushyErrorWithCode(pushy::error_codes::kInvalidOptions, ERROR_OPTIONS)); return; diff --git a/src/__tests__/client.test.ts b/src/__tests__/client.test.ts index 9c846ad4..e652fb42 100644 --- a/src/__tests__/client.test.ts +++ b/src/__tests__/client.test.ts @@ -2274,50 +2274,6 @@ describe('syncNativeConfig', () => { ); }); - test('a host-owned native config (NATIVE_CONFIG_CONFLICT) fails loudly and stops further writes', async () => { - const syncNativeConfig = mock(() => - Promise.reject( - Object.assign(new Error('configure() was called'), { - code: 'NATIVE_CONFIG_CONFLICT', - }) - ) - ); - setupClientMocks({ syncNativeConfig }); - const consoleError = spyOn(console, 'error').mockImplementation(() => {}); - const logger = mock(() => {}); - const { Pushy } = await importFreshClient('sync-config-conflict'); - const client = new Pushy({ appKey: 'demo-app', logger }); - const errors: Array<{ e: any; eventType: string }> = []; - client.onError((e: any, eventType: string) => { - errors.push({ e, eventType }); - }); - await Promise.resolve(); - await Promise.resolve(); - await Promise.resolve(); - - expect(errors).toHaveLength(1); - expect(errors[0].e.code).toBe('NATIVE_CONFIG_CONFLICT'); - expect(errors[0].eventType).toBe('errorNativeConfig'); - expect(consoleError).toHaveBeenCalledWith( - expect.any(String), - expect.stringContaining('error_native_config_conflict') - ); - await Promise.resolve(); - expect(logger).toHaveBeenCalledWith( - expect.objectContaining({ - type: 'errorNativeConfig', - data: expect.objectContaining({ code: 'NATIVE_CONFIG_CONFLICT' }), - }) - ); - - // Retrying would only repeat the same rejection. - client.setOptions({ updateStrategy: 'silentAndNow' }); - await Promise.resolve(); - expect(syncNativeConfig).toHaveBeenCalledTimes(1); - expect(errors).toHaveLength(1); - consoleError.mockRestore(); - }); - test('an older native module without the method is skipped silently', async () => { setupClientMocks(); const { Pushy } = await importFreshClient('sync-config-4'); diff --git a/src/client.ts b/src/client.ts index 7c864bdd..84997b4b 100644 --- a/src/client.ts +++ b/src/client.ts @@ -64,7 +64,6 @@ import { fetchWithTimeout, info, log, - error as logError, noop, promiseAny, setDebugLogging, @@ -255,9 +254,6 @@ export class Pushy { private syncedNativeConfigJson?: string; private pendingNativeConfigJson?: string; private nativeConfigSyncInFlight = false; - // Set once native rejected a write with NATIVE_CONFIG_CONFLICT: the host - // owns the config, so further writes would only repeat the same error. - private nativeConfigConflict = false; // Config last acknowledged by markJsCheckCompleted. private jsCheckCompletedConfigJson?: string; private reportedInvalidUpdates = new Set(); @@ -443,7 +439,7 @@ export class Pushy { this.syncedNativeConfigJson = undefined; return; } - if (this.nativeConfigSyncInFlight || this.nativeConfigConflict) { + if (this.nativeConfigSyncInFlight) { return; } const configJson = this.pendingNativeConfigJson; @@ -470,10 +466,6 @@ export class Pushy { } }) .catch((e: any) => { - if (readErrorCode(e).code === 'NATIVE_CONFIG_CONFLICT') { - this.handleNativeConfigConflict(e); - return; - } log('syncNativeConfig failed:', e?.message || e); }) .finally(() => { @@ -482,24 +474,6 @@ export class Pushy { }); }; - /** - * The host app called the native configure() API in this process, so it - * owns the persisted config, yet JS is in the default 'javascript' mode and - * tried to overwrite it. That is an integration error: say so loudly and - * route it through the error pipeline instead of silently dropping writes. - */ - private handleNativeConfigConflict = (e: unknown) => { - this.nativeConfigConflict = true; - this.pendingNativeConfigJson = undefined; - const err = new UpdateError( - this.t('error_native_config_conflict'), - 'NATIVE_CONFIG_CONFLICT', - { cause: e } - ); - logError(err.message); - this.emitError(err, 'errorNativeConfig'); - }; - /** * Tell the native cold-start orchestrator that this process already holds a * valid check response for the current config, so its delayed round (5s diff --git a/src/error.ts b/src/error.ts index 10a62fb6..df9b6e9f 100644 --- a/src/error.ts +++ b/src/error.ts @@ -36,10 +36,7 @@ export type UpdateErrorCode = | 'INVALID_HASH_INFO' | 'UNSUPPORTED_PLATFORM' | 'APK_INSTALL_PERMISSION_REQUIRED' - | 'APK_INSTALL_FAILED' - // syncNativeConfig refused: the host called the native configure() API in - // this process, but JS still runs in nativeConfigSource: 'javascript'. - | 'NATIVE_CONFIG_CONFLICT'; + | 'APK_INSTALL_FAILED'; const KNOWN_CODES = new Set([ 'MODULE_NOT_LOADED', @@ -64,7 +61,6 @@ const KNOWN_CODES = new Set([ 'UNSUPPORTED_PLATFORM', 'APK_INSTALL_PERMISSION_REQUIRED', 'APK_INSTALL_FAILED', - 'NATIVE_CONFIG_CONFLICT', ]); /** diff --git a/src/locales/en.ts b/src/locales/en.ts index e9fb9da5..502b50a7 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -50,8 +50,6 @@ export default { error_apk_download_failed: 'Failed to download or install the APK', error_reset_not_supported: 'resetToPackagedBundle is not available in the installed native module. Please rebuild the app with the latest react-native-update.', - error_native_config_conflict: - 'The native configure() API was called in this app, so the native side owns the update configuration, but the JS client is in its default nativeConfigSource: "javascript" mode. Set nativeConfigSource: "native" in the client options (and keep appKey/server consistent with the native configuration).', // Development messages dev_debug_disabled: diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 18acd20a..c11f6211 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -45,8 +45,6 @@ export default { error_apk_download_failed: '安装包下载或安装失败', error_reset_not_supported: '当前安装的原生模块不支持 resetToPackagedBundle,请使用最新版 react-native-update 重新打包后再试。', - error_native_config_conflict: - '应用已调用原生 configure() 接口,更新配置归原生侧所有,但 JS 客户端仍处于默认的 nativeConfigSource: "javascript" 模式。请在客户端选项中设置 nativeConfigSource: "native"(并保持 appKey/server 与原生配置一致)。', // Development messages dev_debug_disabled: diff --git a/src/type.ts b/src/type.ts index 3b4ffe5a..6931c273 100644 --- a/src/type.ts +++ b/src/type.ts @@ -93,9 +93,6 @@ export type EventType = | 'crashRescue' | 'reset' | 'errorReset' - // The host app configured the native module itself while JS still claims - // ownership of the native config (NATIVE_CONFIG_CONFLICT). Local only. - | 'errorNativeConfig' // Server reported bundleStatus: unknownBundle — the installed binary is not // registered on the update platform (developer-facing only, local logger; // not a server telemetry event). From 6d07d1360f5fe71303df949135034437b250be66 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 11:58:38 +0800 Subject: [PATCH 18/21] test(e2e-harmony): cover the package diff and bundleHash identity The Harmony suite never exercised the package diff (patching against the bundle embedded in the hap), and nothing checked that the bundleHash the client reports matches that bundle. Both paths now read the rawfile via an fd, so cover them on device: - prepare builds v3 for harmony and, once the hap exists, diffs it with hdiffFromApp; build-harmony-e2e.sh reruns that step after assembleHap - the local server serves pdiff v3 to harmony and records the last bundleHash each platform sent (/control/last-bundle-hash) - the suite asserts bundleHash == sha256(hap rawfile bundle) and walks v2 -> pdiff v3 -> v2-track v4 Co-Authored-By: Claude Opus 5.5 --- Example/e2etest/e2e/harmony/README.md | 7 +- .../e2etest/e2e/harmony/local-update.test.ts | 44 +++++++++- Example/e2etest/scripts/build-harmony-e2e.sh | 7 +- Example/e2etest/scripts/local-e2e-server.ts | 27 ++++-- .../scripts/prepare-local-update-artifacts.ts | 86 ++++++++++++++++--- Example/harmony_use_pushy/e2e/entry.v3.ts | 8 ++ .../e2e/localUpdateConfig.ts | 1 + 7 files changed, 160 insertions(+), 20 deletions(-) create mode 100644 Example/harmony_use_pushy/e2e/entry.v3.ts diff --git a/Example/e2etest/e2e/harmony/README.md b/Example/e2etest/e2e/harmony/README.md index 008687ee..cd5a582e 100644 --- a/Example/e2etest/e2e/harmony/README.md +++ b/Example/e2etest/e2e/harmony/README.md @@ -19,7 +19,12 @@ RNU_E2E_SKIP_PREPARE=true npm run test:e2e:harmony ``` 套件:`local-update.test.ts`(完整更新流:BINARY_BASE → v1 全量 → v2 ppk 差量 → -重启持久化 → upToDate);`smoke.test.ts`(驱动自检)。 +bundleHash 与 hap 内 `rawfile/bundle.harmony.js` 的 sha256 一致 → v3 包差量(以 hap +内置 bundle 为源)→ v4 v2-track 差量 → 重启持久化 → upToDate);`smoke.test.ts`(驱动自检)。 + +v3 包差量以已安装 hap 为源,只能在 assembleHap 之后生成:`build-harmony-e2e.sh` +会在出包后以 `E2E_HARMONY_PACKAGE_DIFF_ONLY=true` 补跑这一步。换了 hap 却没重新生成 +差量(例如只手动 assembleHap),v3 会因源校验失败而卡住。 ## 前置条件 diff --git a/Example/e2etest/e2e/harmony/local-update.test.ts b/Example/e2etest/e2e/harmony/local-update.test.ts index e9a6d6f6..06f959c1 100644 --- a/Example/e2etest/e2e/harmony/local-update.test.ts +++ b/Example/e2etest/e2e/harmony/local-update.test.ts @@ -1,7 +1,8 @@ /** * Harmony update-flow e2e, mirroring the Detox local-merge suite: - * binary base -> full package (v1) -> ppk diff (v2) -> v2-track diff (v4) - * -> persistence. + * binary base -> full package (v1) -> ppk diff (v2) -> package diff against + * the hap's embedded bundle (v3) -> v2-track diff (v4) -> persistence, plus + * the bundleHash identity check. * * Prerequisites: * - Emulator/device in `hdc list targets` @@ -9,6 +10,9 @@ * the e2e entry (e2e/entry.base.ts) installed on the device * - Artifacts + local server are handled by globalSetup (E2E_PLATFORM=harmony) */ +import { execFileSync } from 'node:child_process'; +import { createHash } from 'node:crypto'; +import * as path from 'node:path'; import { HarmonyDriver } from '../harness/harmony-driver.ts'; const BUNDLE_NAME = @@ -17,6 +21,7 @@ const LABELS = { base: 'BINARY_BASE', full: 'E2E_FULL_V1', ppkPatch: 'E2E_PPK_PATCH_V2', + packagePatch: 'E2E_PACKAGE_PATCH_V3', v2Track: 'E2E_V2TRACK_V4', } as const; const LOCAL_UPDATE_PORT = 31337; @@ -32,8 +37,32 @@ const MAX_CHECK_UPDATE_ATTEMPTS = 2; // no such hook, so wait out the default delay with margin instead. const MARK_SUCCESS_SETTLE_MS = 3000; +const HAP_PATH = path.resolve( + __dirname, + '../../../harmony_use_pushy/harmony/entry/build/default/outputs/default/entry-default-signed.hap' +); +const HAP_BUNDLE_ENTRY = 'resources/rawfile/bundle.harmony.js'; + const driver = new HarmonyDriver(BUNDLE_NAME); +function embeddedBundleSha256(): string { + const bundle = execFileSync('unzip', ['-p', HAP_PATH, HAP_BUNDLE_ENTRY], { + maxBuffer: 256 * 1024 * 1024, + }); + if (bundle.length === 0) { + throw new Error(`${HAP_BUNDLE_ENTRY} not found in ${HAP_PATH}`); + } + return createHash('sha256').update(bundle).digest('hex'); +} + +async function lastReportedBundleHash(): Promise { + const response = await fetch( + `http://127.0.0.1:${LOCAL_UPDATE_PORT}/control/last-bundle-hash?platform=harmony` + ); + const body = (await response.json()) as { bundleHash?: string }; + return body.bundleHash ?? ''; +} + async function waitForBundleLabel(expectedLabel: string, timeout: number) { const expectedText = `bundleLabel: ${expectedLabel}`; await driver.waitFor( @@ -95,6 +124,17 @@ describe('harmony local update flow', () => { await tapCheckUpdateAndWaitForBundleLabel(LABELS.ppkPatch); }); + it('reports the embedded bundle sha256 as bundleHash', async () => { + // Every checkUpdate so far carried the hash the native side computed from + // the rawfile fd. A mismatch would make the server trust a pdiff whose + // origin check then fails on device. + expect(await lastReportedBundleHash()).toBe(embeddedBundleSha256()); + }); + + it('applies the package diff against the embedded bundle (v3)', async () => { + await tapCheckUpdateAndWaitForBundleLabel(LABELS.packagePatch); + }); + it('applies the v2-track diff update (v4)', async () => { await tapCheckUpdateAndWaitForBundleLabel(LABELS.v2Track); }); diff --git a/Example/e2etest/scripts/build-harmony-e2e.sh b/Example/e2etest/scripts/build-harmony-e2e.sh index 621c918b..a4f74b30 100755 --- a/Example/e2etest/scripts/build-harmony-e2e.sh +++ b/Example/e2etest/scripts/build-harmony-e2e.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # Build & install the HarmonyOS e2e base app, encoding the required order: # local har -> swap local react-native-update -> refresh oh_modules -# -> prepare update artifacts -> base bundle -> assembleHap -> install. +# -> prepare update artifacts -> base bundle -> assembleHap +# -> package diff against that hap -> install. # # The order matters: `pushy bundle --platform harmony` (run by the prepare # step) overwrites the project's rawfile/bundle.harmony.js, so the base bundle @@ -77,6 +78,10 @@ echo "==> Assembling signed hap" exit 1 } +echo "==> Generating package diff against the hap's embedded bundle" +(cd "$E2ETEST_ROOT" && E2E_PLATFORM=harmony E2E_HARMONY_PACKAGE_DIFF_ONLY=true \ + node scripts/run-prepare-local-update-artifacts.js) + if [ "${SKIP_INSTALL:-false}" != "true" ]; then echo "==> Installing $BUNDLE_NAME" hdc list targets | grep -qv '^\[Empty\]$' || { diff --git a/Example/e2etest/scripts/local-e2e-server.ts b/Example/e2etest/scripts/local-e2e-server.ts index 7cbdfab9..21ec09b5 100644 --- a/Example/e2etest/scripts/local-e2e-server.ts +++ b/Example/e2etest/scripts/local-e2e-server.ts @@ -70,6 +70,10 @@ function safeResolve(urlPath: string) { return target; } +// Most recent non-empty bundleHash each platform sent with checkUpdate, so a +// test can compare it against the sha256 of the bundle embedded in the binary. +const lastBundleHash: Record = {}; + function buildUpdateResponse( platform: string, currentHash: string, @@ -104,12 +108,15 @@ function buildUpdateResponse( }; } - if (platform === 'android' && currentHash === LOCAL_UPDATE_HASHES.ppkPatch) { + // Package diffs (patched against the bundle embedded in the installed + // binary) run on Android and Harmony; iOS has no package-diff artifact. + const servesPackageDiff = platform === 'android' || platform === 'harmony'; + if (servesPackageDiff && currentHash === LOCAL_UPDATE_HASHES.ppkPatch) { return { update: true, name: 'local-pdiff-v3', hash: LOCAL_UPDATE_HASHES.packagePatch, - description: 'Serve an Android package diff from local e2e server.', + description: 'Serve a package diff from local e2e server.', metaInfo: JSON.stringify({ stage: 'pdiff', platform }), paths: [assetBasePath], pdiff: LOCAL_UPDATE_FILES.packageDiff, @@ -122,10 +129,9 @@ function buildUpdateResponse( platform, LOCAL_UPDATE_FILES.v2TrackDiff ); - const v2TrackPrevHash = - platform === 'android' - ? LOCAL_UPDATE_HASHES.packagePatch - : LOCAL_UPDATE_HASHES.ppkPatch; + const v2TrackPrevHash = servesPackageDiff + ? LOCAL_UPDATE_HASHES.packagePatch + : LOCAL_UPDATE_HASHES.ppkPatch; if (currentHash === v2TrackPrevHash && diffV >= 2) { if (fullFallback) { return { @@ -177,6 +183,11 @@ const server = Bun.serve({ }); } + if (url.pathname === '/control/last-bundle-hash') { + const target = url.searchParams.get('platform') || ''; + return json({ bundleHash: lastBundleHash[target] ?? '' }); + } + if (url.pathname === '/control/force-boot') { if (request.method !== 'POST') { return new Response('method not allowed', { status: 405 }); @@ -202,7 +213,11 @@ const server = Bun.serve({ const payload = (await request.json().catch(() => ({}))) as { hash?: unknown; diffV?: unknown; + bundleHash?: unknown; }; + if (typeof payload.bundleHash === 'string' && payload.bundleHash) { + lastBundleHash[platform] = payload.bundleHash; + } const currentHash = typeof payload.hash === 'string' ? payload.hash : ''; const diffV = typeof payload.diffV === 'number' ? payload.diffV : 0; diff --git a/Example/e2etest/scripts/prepare-local-update-artifacts.ts b/Example/e2etest/scripts/prepare-local-update-artifacts.ts index e1c3051e..78f2bec5 100644 --- a/Example/e2etest/scripts/prepare-local-update-artifacts.ts +++ b/Example/e2etest/scripts/prepare-local-update-artifacts.ts @@ -24,16 +24,19 @@ type DiffCommandRunner = { 'no-interactive': true; }; }) => Promise; - hdiffFromApk: (options: { - args: [string, string]; - options: { - output: string; - customDiff: (oldSource?: Buffer, newSource?: Buffer) => Buffer; - 'no-interactive': true; - }; - }) => Promise; + hdiffFromApk: PackageDiffCommand; + hdiffFromApp: PackageDiffCommand; }; +type PackageDiffCommand = (options: { + args: [string, string]; + options: { + output: string; + customDiff: (oldSource?: Buffer, newSource?: Buffer) => Buffer; + 'no-interactive': true; + }; +}) => Promise; + type HdiffModule = { diff?: (oldSource?: Buffer, newSource?: Buffer) => Buffer; patch?: (oldSource?: Buffer, patchSource?: Buffer) => Buffer; @@ -109,6 +112,14 @@ if (!['ios', 'android', 'harmony'].includes(platform)) { const harmonyProjectRoot = path.resolve(projectRoot, '../harmony_use_pushy'); const bundleProjectRoot = platform === 'harmony' ? harmonyProjectRoot : projectRoot; +// The signed release hap the device runs. Its rawfile bundle is the pdiff +// origin, so the package diff can only be made once this hap exists — which is +// after artifact prep (build-harmony-e2e.sh reruns this script with +// E2E_HARMONY_PACKAGE_DIFF_ONLY=true right after assembleHap). +const harmonyHapPath = path.join( + harmonyProjectRoot, + 'harmony/entry/build/default/outputs/default/entry-default-signed.hap' +); if (!fs.existsSync(cliEntry)) { throw new Error(`react-native-update-cli entry not found: ${cliEntry}`); @@ -431,7 +442,51 @@ async function generateAndroidPackageDiff( verifyGeneratedFile('package diff', output); } +async function generateHarmonyPackageDiff(v3: string) { + const output = path.join(artifactsDir, LOCAL_UPDATE_FILES.packageDiff); + if (!fs.existsSync(harmonyHapPath)) { + // Fresh checkout: build-harmony-e2e.sh produces the hap and comes back + // for this step. The server would serve a broken pdiff otherwise. + console.warn( + `Harmony hap not found yet (${harmonyHapPath}); package diff deferred.` + ); + writeFallbackPatch('package diff', output); + return; + } + if (useFullFallbackArtifacts) { + writeFallbackPatch('package diff', output); + return; + } + const { customDiff } = ensureHdiffModule(); + console.log( + `Running hdiffFromApp: ${harmonyHapPath} -> ${v3} (${ + fs.statSync(harmonyHapPath).size + } -> ${fs.statSync(v3).size} bytes)` + ); + await keepProcessAlive( + 'harmony package diff', + diffCommands.hdiffFromApp({ + args: [harmonyHapPath, v3], + options: { output, customDiff, 'no-interactive': true }, + }) + ); + verifyGeneratedFile('harmony package diff', output); +} + async function main() { + if ( + platform === 'harmony' && + process.env.E2E_HARMONY_PACKAGE_DIFF_ONLY === 'true' + ) { + const v3 = path.join(artifactsDir, LOCAL_UPDATE_FILES.packageFull); + if (!fs.existsSync(v3)) { + throw new Error( + `v3 artifact missing: ${v3}; run the full prepare first.` + ); + } + await generateHarmonyPackageDiff(v3); + return; + } prepareDir(); const v1 = path.join(artifactsDir, LOCAL_UPDATE_FILES.full); @@ -445,7 +500,7 @@ async function main() { // hermesc -base-bytecode=v1, so every patch below carries delta-mode bytecode. bundleTo('e2e/entry.v1.ts', v1); bundleTo('e2e/entry.v2.ts', v2, v1); - if (platform === 'android') { + if (platform === 'android' || platform === 'harmony') { bundleTo('e2e/entry.v3.ts', v3, v1); } bundleTo('e2e/entry.v4.ts', v4, v1); @@ -519,8 +574,19 @@ async function main() { console.log('Generating ppk diff...'); await generatePpkDiff(v1, v2, ppkDiff, hdiff.customDiff); + // Harmony runs the same chain as Android (v2 -> pdiff v3 -> v4), so + // its v2-track diff starts from v3. console.log('Generating v2-track ppk diff...'); - await generateV2TrackPpkDiff(v2, v4, v2TrackDiff, hdiff); + await generateV2TrackPpkDiff( + platform === 'harmony' ? v3 : v2, + v4, + v2TrackDiff, + hdiff + ); + } + if (platform === 'harmony') { + console.log('Generating harmony package diff...'); + await generateHarmonyPackageDiff(v3); } } diff --git a/Example/harmony_use_pushy/e2e/entry.v3.ts b/Example/harmony_use_pushy/e2e/entry.v3.ts new file mode 100644 index 00000000..217a8fc4 --- /dev/null +++ b/Example/harmony_use_pushy/e2e/entry.v3.ts @@ -0,0 +1,8 @@ +import { LOCAL_UPDATE_LABELS } from './localUpdateConfig'; + +const bundleLabelGlobal = globalThis as typeof globalThis & { + __RNU_E2E_BUNDLE_LABEL?: string; +}; + +bundleLabelGlobal.__RNU_E2E_BUNDLE_LABEL = LOCAL_UPDATE_LABELS.packagePatch; +require('./entry.base'); diff --git a/Example/harmony_use_pushy/e2e/localUpdateConfig.ts b/Example/harmony_use_pushy/e2e/localUpdateConfig.ts index 2756ed6c..3ca31e1a 100644 --- a/Example/harmony_use_pushy/e2e/localUpdateConfig.ts +++ b/Example/harmony_use_pushy/e2e/localUpdateConfig.ts @@ -9,6 +9,7 @@ export const LOCAL_UPDATE_LABELS = { base: 'BINARY_BASE', full: 'E2E_FULL_V1', ppkPatch: 'E2E_PPK_PATCH_V2', + packagePatch: 'E2E_PACKAGE_PATCH_V3', v2Track: 'E2E_V2TRACK_V4', } as const; From b81a5c29f89aaabc42a61ed86e013c6f88ac8685 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 12:00:36 +0800 Subject: [PATCH 19/21] fix: address CodeRabbit review on #654 - ios: run host prepareBundle calls on a concurrent queue so each timeout counts from its own call (a serial queue queued a timed call behind an earlier call's full wait) - android: a failed trash purge is logged, not a failed cleanup task - android: restore the configure() note that JS writes can replace the config unless nativeConfigSource is 'native' - ci: run the C++ parity job when the TS reference or vector generator changes - deps: react / react-test-renderer 19.2.3 to match react-native 0.85 - js: clarify when the native check cache is marked exhausted Co-Authored-By: Claude Opus 5.5 --- .github/workflows/test.yml | 3 +++ .../modules/update/DownloadTask.java | 8 +++++++- .../modules/update/PushyRuntime.java | 3 ++- bun.lock | 20 ++++++++----------- ios/RCTPushy/RCTPushy.mm | 10 +++++++--- package.json | 6 +++--- src/client.ts | 4 +++- 7 files changed, 33 insertions(+), 21 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 05912f00..72618738 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -40,6 +40,9 @@ jobs: - 'cpp/**' - 'android/jni/**' - 'scripts/test-*' + # TS reference the C++ golden vectors are generated from. + - 'src/updateFlowCore.ts' + - 'scripts/generate-flow-vectors.ts' ios: - '.github/workflows/test.yml' - 'ios/**' diff --git a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java index f92ede86..432536df 100644 --- a/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java +++ b/android/src/main/java/cn/reactnative/modules/update/DownloadTask.java @@ -781,8 +781,14 @@ public void run(String[] keepNames) { ); // Phase 2, lock released: nothing in the trash is reachable as a // version any more. Also purges leftovers of an interrupted run. + // Best effort: phase 1 already succeeded, and a leftover the native + // delete cannot remove must not fail every later cleanup task. if (trash.isDirectory()) { - cleanupOldEntries(trash.getAbsolutePath(), new String[0], 0, null); + try { + cleanupOldEntries(trash.getAbsolutePath(), new String[0], 0, null); + } catch (RuntimeException e) { + Log.w(Texts.LOG_TAG, "Failed to purge cleanup trash", e); + } } } diff --git a/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java index df9d5e84..38123909 100644 --- a/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java +++ b/android/src/main/java/cn/reactnative/modules/update/PushyRuntime.java @@ -46,7 +46,8 @@ 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/prepareBundle. + * 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) { if (context == null || options == null || callback == null) { diff --git a/bun.lock b/bun.lock index 63083678..5076326b 100644 --- a/bun.lock +++ b/bun.lock @@ -10,10 +10,10 @@ "@types/bun": "^1.3.11", "@types/node": "^25.5.0", "@types/react": "^19.2.14", - "@types/react-test-renderer": "18.0.7", - "react": "18.2.0", + "@types/react-test-renderer": "19.1.0", + "react": "19.2.3", "react-native": "0.85.2", - "react-test-renderer": "18.2.0", + "react-test-renderer": "19.2.3", "typescript": "^7.0.2", }, "peerDependencies": { @@ -131,7 +131,7 @@ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@types/react-test-renderer": ["@types/react-test-renderer@18.0.7", "", { "dependencies": { "@types/react": "*" } }, "sha512-1+ANPOWc6rB3IkSnElhjv6VLlKg2dSv/OWClUyZimbLsQyBn8Js9Vtdsi3UICJ2rIQ3k2la06dkB+C92QfhKmg=="], + "@types/react-test-renderer": ["@types/react-test-renderer@19.1.0", "", { "dependencies": { "@types/react": "*" } }, "sha512-XD0WZrHqjNrxA/MaR9O22w/RNidWR9YZmBdRGI7wcnWGrv/3dA8wKCJ8m63Sn+tLJhcjmuhOi629N66W6kgWzQ=="], "@types/yargs": ["@types/yargs@17.0.33", "", { "dependencies": { "@types/yargs-parser": "*" } }, "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA=="], @@ -397,8 +397,6 @@ "ob1": ["ob1@0.84.6", "", { "dependencies": { "flow-enums-runtime": "^0.0.6" } }, "sha512-+s+6zjd0X68hfAcn92NsEPXnSYOF3O7exw/Fj4UfRTB+UL+Tdjdkzu5/4WquXlzqgvNm8FWuuW6/0Yd8S2rYxw=="], - "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], - "on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], "open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], @@ -417,19 +415,17 @@ "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], - "react": ["react@18.2.0", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ=="], + "react": ["react@19.2.3", "", {}, "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA=="], "react-devtools-core": ["react-devtools-core@6.1.5", "", { "dependencies": { "shell-quote": "^1.6.1", "ws": "^7" } }, "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA=="], - "react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], + "react-is": ["react-is@19.3.0", "", {}, "sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA=="], "react-native": ["react-native@0.85.2", "", { "dependencies": { "@react-native/assets-registry": "0.85.2", "@react-native/codegen": "0.85.2", "@react-native/community-cli-plugin": "0.85.2", "@react-native/gradle-plugin": "0.85.2", "@react-native/js-polyfills": "0.85.2", "@react-native/normalize-colors": "0.85.2", "@react-native/virtualized-lists": "0.85.2", "abort-controller": "^3.0.0", "anser": "^1.4.9", "ansi-regex": "^5.0.0", "babel-plugin-syntax-hermes-parser": "0.33.3", "base64-js": "^1.5.1", "commander": "^12.0.0", "flow-enums-runtime": "^0.0.6", "hermes-compiler": "250829098.0.10", "invariant": "^2.2.4", "memoize-one": "^5.0.0", "metro-runtime": "^0.84.0", "metro-source-map": "^0.84.0", "nullthrows": "^1.1.1", "pretty-format": "^29.7.0", "promise": "^8.3.0", "react-devtools-core": "^6.1.5", "react-refresh": "^0.14.0", "regenerator-runtime": "^0.13.2", "scheduler": "0.27.0", "semver": "^7.1.3", "stacktrace-parser": "^0.1.10", "tinyglobby": "^0.2.15", "whatwg-fetch": "^3.0.0", "ws": "^7.5.10", "yargs": "^17.6.2" }, "peerDependencies": { "@react-native/jest-preset": "0.85.2", "@types/react": "^19.1.1", "react": "^19.2.3" }, "optionalPeers": ["@react-native/jest-preset", "@types/react"], "bin": { "react-native": "cli.js" } }, "sha512-GFWEPwLYirfj5X8gMtXOWtqX0cqUEURRHETZfFk37VCa4++izrKvGvv24anvuyulXV87NAhVkfNw93rLg3HByw=="], "react-refresh": ["react-refresh@0.14.2", "", {}, "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA=="], - "react-shallow-renderer": ["react-shallow-renderer@16.15.0", "", { "dependencies": { "object-assign": "^4.1.1", "react-is": "^16.12.0 || ^17.0.0 || ^18.0.0" }, "peerDependencies": { "react": "^16.0.0 || ^17.0.0 || ^18.0.0" } }, "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA=="], - - "react-test-renderer": ["react-test-renderer@18.2.0", "", { "dependencies": { "react-is": "^18.2.0", "react-shallow-renderer": "^16.15.0", "scheduler": "^0.23.0" }, "peerDependencies": { "react": "^18.2.0" } }, "sha512-JWD+aQ0lh2gvh4NM3bBM42Kx+XybOxCpgYK7F8ugAlpaTSnWsX+39Z4XkOykGZAHrjwwTZT3x3KxswVWxHPUqA=="], + "react-test-renderer": ["react-test-renderer@19.2.3", "", { "dependencies": { "react-is": "^19.2.3", "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.3" } }, "sha512-TMR1LnSFiWZMJkCgNf5ATSvAheTT2NvKIwiVwdBPHxjBI7n/JbWd4gaZ16DVd9foAXdvDz+sB5yxZTwMjPRxpw=="], "regenerator-runtime": ["regenerator-runtime@0.13.11", "", {}, "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="], @@ -597,7 +593,7 @@ "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], - "react-test-renderer/scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], + "pretty-format/react-is": ["react-is@18.3.1", "", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="], "send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], diff --git a/ios/RCTPushy/RCTPushy.mm b/ios/RCTPushy/RCTPushy.mm index 3245e2cf..cbcef107 100644 --- a/ios/RCTPushy/RCTPushy.mm +++ b/ios/RCTPushy/RCTPushy.mm @@ -830,14 +830,18 @@ PUSHY_HIDDEN void PushySwitchVersionLocked(NSString *hash) { } } -// Host preparation runs off the caller's thread, one at a time; the -// completion is delivered on the main queue. +// Host preparation runs off the caller's thread; the completion is delivered +// on the main queue. Calls run concurrently so each timeout counts from its +// own call: a serial queue would hold a later prepareBundle(timeout:) behind an +// earlier call's whole wait. Nothing here needs serializing — claimRound is an +// atomic compare-exchange and the round group (entered once at setup) releases +// every waiter when the round ends. static void PushyRunHostPreparation(NSDictionary *(^work)(void), RCTPushyBundlePreparationCompletion completion) { static dispatch_queue_t hostQueue; static dispatch_once_t once; dispatch_once(&once, ^{ - hostQueue = dispatch_queue_create("cn.reactnative.pushy.host-sync", DISPATCH_QUEUE_SERIAL); + hostQueue = dispatch_queue_create("cn.reactnative.pushy.host-sync", DISPATCH_QUEUE_CONCURRENT); }); dispatch_async(hostQueue, ^{ NSDictionary *result; diff --git a/package.json b/package.json index 281e2b43..6b53ba34 100644 --- a/package.json +++ b/package.json @@ -98,10 +98,10 @@ "@types/bun": "^1.3.11", "@types/node": "^25.5.0", "@types/react": "^19.2.14", - "@types/react-test-renderer": "18.0.7", - "react": "18.2.0", + "@types/react-test-renderer": "19.1.0", + "react": "19.2.3", "react-native": "0.85.2", - "react-test-renderer": "18.2.0", + "react-test-renderer": "19.2.3", "typescript": "^7.0.2" } } diff --git a/src/client.ts b/src/client.ts index 84997b4b..e0cd24d5 100644 --- a/src/client.ts +++ b/src/client.ts @@ -161,7 +161,9 @@ export const sharedState: { // The native cold-start check writes its response cache at most once per // launch (and skips it once JS has checked), so once this process has seen it // absent or expired, later reads are near-certain misses: skip the bridge -// call from then on. The rare miss costs one network check. +// call from then on. The rare miss costs one network check. A fresh entry +// that only mismatches the request or config does not set this: a later check +// with the original options (e.g. after `extra` reverts) can still reuse it. let nativeCheckCacheExhausted = false; const NATIVE_CHECK_CACHE_MAX_AGE_S = 120; From e6c9e50e3b9788cf3626cf0e233b174030d28036 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 12:01:48 +0800 Subject: [PATCH 20/21] test: share one C1 config-equivalence vector file across all four platforms cpp/update_flow_core/tests/config-equivalence-vectors.json (58 cases, not packed: !cpp/**/tests) is now the single contract for native config equivalence. Each case is checked in both argument orders by: - JS + Harmony: src/__tests__/configEquivalenceVectors.test.ts (Harmony's UpdateContext.ts evaluated in a vm with imports stripped) - Android: ConfigEquivalenceTest (locates the file by walking up from the module dir, works in-repo and from node_modules) - iOS: the purge-restore harness's config_equivalence case The vectors exposed two outliers, fixed to the agreed rule: - JS did not dedupe non-string entries of endpoints/queryUrls ([1,1] stayed [1,1]); Android, iOS and Harmony dedupe by JSON value. - Android accepted trailing content after the object ('{"a":1} x' and two concatenated objects) because JSONTokener.nextValue() stops after the first value; JS, iOS and Harmony reject it as invalid JSON. Co-Authored-By: Claude Opus 5.5 --- .../modules/update/ConfigEquivalence.java | 8 +- .../modules/update/ConfigEquivalenceTest.java | 84 +++-- .../tests/config-equivalence-vectors.json | 353 ++++++++++++++++++ scripts/tests/ios-purge-restore/README.md | 7 +- .../ios-purge-restore/purge_restore_test.mm | 40 +- .../configEquivalenceVectors.test.ts | 62 +++ src/updateFlowCore.ts | 68 ++-- 7 files changed, 525 insertions(+), 97 deletions(-) create mode 100644 cpp/update_flow_core/tests/config-equivalence-vectors.json create mode 100644 src/__tests__/configEquivalenceVectors.test.ts diff --git a/android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java b/android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java index c21ea047..a91e97db 100644 --- a/android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java +++ b/android/src/main/java/cn/reactnative/modules/update/ConfigEquivalence.java @@ -48,7 +48,13 @@ static Map canonical(@Nullable String json) { } Object parsed; try { - parsed = new JSONTokener(json).nextValue(); + JSONTokener tokener = new JSONTokener(json); + parsed = tokener.nextValue(); + // nextValue() stops after the first value; anything but trailing + // whitespace after it is not JSON (as JSON.parse/NSJSONSerialization). + if (tokener.nextClean() != 0) { + return null; + } } catch (JSONException e) { return null; } diff --git a/android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java b/android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java index 85246e21..6508947b 100644 --- a/android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java +++ b/android/src/test/java/cn/reactnative/modules/update/ConfigEquivalenceTest.java @@ -1,57 +1,63 @@ package cn.reactnative.modules.update; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import org.json.JSONArray; +import org.json.JSONObject; import org.junit.Test; public class ConfigEquivalenceTest { - @Test - public void ignoresKeyOrder() { - assertTrue(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\",\"endpoints\":[\"https://a\"]}", - "{\"endpoints\":[\"https://a\"],\"appKey\":\"k\"}")); - } + private static final String VECTORS = + "cpp/update_flow_core/tests/config-equivalence-vectors.json"; - @Test - public void fillsDefaultsForAbsentKeys() { - assertTrue(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\"}", - "{\"appKey\":\"k\",\"disabled\":false,\"queryUrls\":[],\"rnu\":\"\"," - + "\"rn\":\"\",\"afterDownload\":\"none\"}")); - assertFalse(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\"}", "{\"appKey\":\"k\",\"disabled\":true}")); - assertFalse(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\"}", "{\"appKey\":\"k\",\"rn\":\"0.80.0\"}")); - } - - @Test - public void normalizesUrlListsKeepingFirstOccurrence() { - assertTrue(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\",\"endpoints\":[\"https://a/\",\"https://b\",\"https://a\"]," - + "\"queryUrls\":[\"https://q//\",\"https://q\"]}", - "{\"appKey\":\"k\",\"endpoints\":[\"https://a\",\"https://b\"]," - + "\"queryUrls\":[\"https://q\"]}")); - // Order is meaningful: it is the fallback order. - assertFalse(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\",\"endpoints\":[\"https://a\",\"https://b\"]}", - "{\"appKey\":\"k\",\"endpoints\":[\"https://b\",\"https://a\"]}")); + // The shared C1 vectors live at the package root (next to android/). Gradle + // runs unit tests from the module directory, which is android/ in this repo + // and node_modules/react-native-update/android in a host app; walk up from + // there so both layouts (and symlinked copies) resolve. + private static File vectorsFile() { + String override = System.getProperty("pushy.configVectors"); + if (override != null) { + return new File(override); + } + for (File dir = new File(System.getProperty("user.dir")).getAbsoluteFile(); + dir != null; dir = dir.getParentFile()) { + File candidate = new File(dir, VECTORS); + if (candidate.isFile()) { + return candidate; + } + } + return null; } @Test - public void detectsRealChanges() { - assertFalse(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\"}", "{\"appKey\":\"other\"}")); - assertFalse(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\"}", "{\"appKey\":\"k\",\"packageVersion\":\"1.0\"}")); - assertTrue(ConfigEquivalence.equivalent( - "{\"appKey\":\"k\",\"n\":1}", "{\"appKey\":\"k\",\"n\":1.0}")); + public void sharedVectors() throws Exception { + File file = vectorsFile(); + assertNotNull("cannot locate " + VECTORS + " above " + System.getProperty("user.dir"), file); + JSONArray cases = new JSONObject( + new String(Files.readAllBytes(file.toPath()), StandardCharsets.UTF_8)) + .getJSONArray("cases"); + assertTrue("vector file has cases", cases.length() >= 30); + for (int i = 0; i < cases.length(); i++) { + JSONObject vector = cases.getJSONObject(i); + String name = vector.getString("name"); + String a = vector.getString("a"); + String b = vector.getString("b"); + boolean equivalent = vector.getBoolean("equivalent"); + assertEquals(name, equivalent, ConfigEquivalence.equivalent(a, b)); + assertEquals(name + " (reversed)", equivalent, ConfigEquivalence.equivalent(b, a)); + } } @Test - public void unparseableIsNeverEquivalent() { + public void missingConfigIsNeverEquivalent() { assertFalse(ConfigEquivalence.equivalent(null, null)); - assertFalse(ConfigEquivalence.equivalent("{", "{")); - assertFalse(ConfigEquivalence.equivalent("[]", "[]")); assertFalse(ConfigEquivalence.equivalent("{\"appKey\":\"k\"}", null)); + assertFalse(ConfigEquivalence.equivalent(null, "{\"appKey\":\"k\"}")); } } diff --git a/cpp/update_flow_core/tests/config-equivalence-vectors.json b/cpp/update_flow_core/tests/config-equivalence-vectors.json new file mode 100644 index 00000000..e93b72b8 --- /dev/null +++ b/cpp/update_flow_core/tests/config-equivalence-vectors.json @@ -0,0 +1,353 @@ +{ + "description": "Contract C1: native config JSON equivalence, replayed by the JS (src/updateFlowCore.ts), Harmony (UpdateContext.ts), Android (ConfigEquivalence.java) and iOS (PushyConfigsEquivalent) implementations. Two strings are equivalent iff both parse to JSON objects whose canonical forms deep-equal (key order ignored). Canonical: absent keys take {disabled:false, queryUrls:[], rnu:\"\", rn:\"\", afterDownload:\"none\"}; each string in endpoints/queryUrls (when an array) loses ALL trailing \"/\", then entries are deduplicated keeping the first occurrence. Present values are never coerced (0 is not false, null is not a default). Anything that is not a JSON object is never equivalent, not even to itself. Every case must hold in both argument orders. Hand-maintained: edit this file, then keep every platform green.", + "cases": [ + { + "name": "identical", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "key order is ignored", + "a": "{\"appKey\":\"k\",\"packageVersion\":\"1.0\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"endpoints\":[\"https://a.example.com\"],\"packageVersion\":\"1.0\",\"appKey\":\"k\"}", + "equivalent": true + }, + { + "name": "absent disabled equals false", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"disabled\":false}", + "equivalent": true + }, + { + "name": "absent queryUrls equals []", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":[]}", + "equivalent": true + }, + { + "name": "absent rnu equals \"\"", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"rnu\":\"\"}", + "equivalent": true + }, + { + "name": "absent rn equals \"\"", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"rn\":\"\"}", + "equivalent": true + }, + { + "name": "absent afterDownload equals \"none\"", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"afterDownload\":\"none\"}", + "equivalent": true + }, + { + "name": "every default explicit vs all absent", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"disabled\":false,\"queryUrls\":[],\"rnu\":\"\",\"rn\":\"\",\"afterDownload\":\"none\",\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "empty object vs explicit defaults", + "a": "{}", + "b": "{\"disabled\":false,\"queryUrls\":[],\"rnu\":\"\",\"rn\":\"\",\"afterDownload\":\"none\"}", + "equivalent": true + }, + { + "name": "disabled true is not the default", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"disabled\":true}", + "equivalent": false + }, + { + "name": "disabled true on both sides", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"disabled\":true}", + "b": "{\"disabled\":true,\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "disabled 0 is not false", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"disabled\":0}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"disabled\":false}", + "equivalent": false + }, + { + "name": "disabled 0 is not the absent default", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"disabled\":0}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "non-default afterDownload is a change", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"afterDownload\":\"setNeedUpdate\"}", + "equivalent": false + }, + { + "name": "non-empty rn is a change", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"rn\":\"0.80.0\"}", + "equivalent": false + }, + { + "name": "null rn is not the absent default", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"rn\":null}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "null queryUrls is not the absent default", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":null}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "null equals null", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"extra\":null}", + "b": "{\"extra\":null,\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "packageVersion present vs absent (no default)", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"packageVersion\":\"1.0\"}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "empty packageVersion vs absent (no default)", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"packageVersion\":\"\"}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "empty appKey vs absent appKey (no default)", + "a": "{\"appKey\":\"\"}", + "b": "{}", + "equivalent": false + }, + { + "name": "different appKey", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"other\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "endpoints [] vs absent endpoints (no default)", + "a": "{\"appKey\":\"k\",\"endpoints\":[]}", + "b": "{\"appKey\":\"k\"}", + "equivalent": false + }, + { + "name": "single trailing slash stripped in endpoints", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com/\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "all trailing slashes stripped in endpoints", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com///\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "trailing slash stripped in queryUrls", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":[\"https://q.example.com/endpoints.json/\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":[\"https://q.example.com/endpoints.json\"]}", + "equivalent": true + }, + { + "name": "slash-only entry becomes empty string", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"//\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"\"]}", + "equivalent": true + }, + { + "name": "trailing path slash stripped, inner slashes kept", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com/api/\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com/api\"]}", + "equivalent": true + }, + { + "name": "inner double slash is significant", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com//api\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com/api\"]}", + "equivalent": false + }, + { + "name": "host case is significant", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://A.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "trailing slashes only normalized in endpoints/queryUrls", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"homepage\":\"https://h.example.com/\"}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"homepage\":\"https://h.example.com\"}", + "equivalent": false + }, + { + "name": "endpoints as a bare string are not normalized", + "a": "{\"appKey\":\"k\",\"endpoints\":\"https://a.example.com/\"}", + "b": "{\"appKey\":\"k\",\"endpoints\":\"https://a.example.com\"}", + "equivalent": false + }, + { + "name": "duplicates dropped after stripping, first occurrence kept", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com/\",\"https://b.example.com\",\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\",\"https://b.example.com\"]}", + "equivalent": true + }, + { + "name": "dedupe keeps first-occurrence order", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://b.example.com\",\"https://a.example.com\",\"https://b.example.com/\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://b.example.com\",\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "dedupe does not sort", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://b.example.com\",\"https://a.example.com\",\"https://b.example.com/\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\",\"https://b.example.com\"]}", + "equivalent": false + }, + { + "name": "queryUrls deduped", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":[\"https://q.example.com//\",\"https://q.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":[\"https://q.example.com\"]}", + "equivalent": true + }, + { + "name": "non-string list entries are deduped too", + "a": "{\"appKey\":\"k\",\"endpoints\":[1,1.0,\"https://a.example.com\",{\"x\":1},{\"x\":1}]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[1,\"https://a.example.com/\",{\"x\":1}]}", + "equivalent": true + }, + { + "name": "endpoint order is significant", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\",\"https://b.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://b.example.com\",\"https://a.example.com\"]}", + "equivalent": false + }, + { + "name": "queryUrls order is significant", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":[\"https://q1.example.com\",\"https://q2.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"queryUrls\":[\"https://q2.example.com\",\"https://q1.example.com\"]}", + "equivalent": false + }, + { + "name": "extra unknown key on one side", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"extra\":1}", + "equivalent": false + }, + { + "name": "same unknown key on both sides", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"extra\":1}", + "b": "{\"extra\":1,\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "number 1 equals 1.0", + "a": "{\"appKey\":\"k\",\"n\":1}", + "b": "{\"appKey\":\"k\",\"n\":1.0}", + "equivalent": true + }, + { + "name": "string \"1\" is not number 1", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"n\":\"1\"}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"n\":1}", + "equivalent": false + }, + { + "name": "nested object key order is ignored", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"extra\":{\"x\":1,\"y\":[1,{\"p\":true,\"q\":null}]}}", + "b": "{\"extra\":{\"y\":[1,{\"q\":null,\"p\":true}],\"x\":1},\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"]}", + "equivalent": true + }, + { + "name": "nested array order is significant", + "a": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"extra\":{\"y\":[1,2]}}", + "b": "{\"appKey\":\"k\",\"endpoints\":[\"https://a.example.com\"],\"extra\":{\"y\":[2,1]}}", + "equivalent": false + }, + { + "name": "unicode is compared without normalization", + "a": "{\"appKey\":\"caf\\u00e9\"}", + "b": "{\"appKey\":\"cafe\\u0301\"}", + "equivalent": false + }, + { + "name": "surrounding whitespace is irrelevant", + "a": " \n{\"appKey\":\"k\"}\t ", + "b": "{\"appKey\":\"k\"}", + "equivalent": true + }, + { + "name": "JSON array is never equivalent, even to itself", + "a": "[]", + "b": "[]", + "equivalent": false + }, + { + "name": "JSON array of an object is not an object", + "a": "[{\"appKey\":\"k\"}]", + "b": "{\"appKey\":\"k\"}", + "equivalent": false + }, + { + "name": "JSON string is never equivalent", + "a": "\"{}\"", + "b": "\"{}\"", + "equivalent": false + }, + { + "name": "JSON number is never equivalent", + "a": "1", + "b": "1", + "equivalent": false + }, + { + "name": "JSON null is never equivalent", + "a": "null", + "b": "null", + "equivalent": false + }, + { + "name": "JSON boolean is never equivalent", + "a": "true", + "b": "true", + "equivalent": false + }, + { + "name": "invalid JSON is never equivalent, even to itself", + "a": "{", + "b": "{", + "equivalent": false + }, + { + "name": "invalid JSON (unterminated) vs valid object", + "a": "{\"appKey\":\"k\"", + "b": "{\"appKey\":\"k\"}", + "equivalent": false + }, + { + "name": "empty string is never equivalent", + "a": "", + "b": "", + "equivalent": false + }, + { + "name": "trailing content after the object is invalid JSON", + "a": "{\"appKey\":\"k\"} x", + "b": "{\"appKey\":\"k\"}", + "equivalent": false + }, + { + "name": "two concatenated objects are invalid JSON", + "a": "{\"appKey\":\"k\"}{\"appKey\":\"k\"}", + "b": "{\"appKey\":\"k\"}{\"appKey\":\"k\"}", + "equivalent": false + } + ] +} diff --git a/scripts/tests/ios-purge-restore/README.md b/scripts/tests/ios-purge-restore/README.md index 255b6696..92a5e2f8 100644 --- a/scripts/tests/ios-purge-restore/README.md +++ b/scripts/tests/ios-purge-restore/README.md @@ -85,9 +85,10 @@ preferences domain. | `offline_skip` | Network-path probe reports no path | No window, no round, no wait; packaged bundle launches | | `spent_budget_skip` | 10.5s already elapsed (under the 2s minimum left) | Same as offline: the restore is not attempted | -`config_equivalence` additionally checks the cross-platform canonical config -equality (defaults, key order, trailing slashes, duplicates; `0` is not -`false`; unparseable is never equivalent). +`config_equivalence` additionally replays the cross-platform canonical config +equality vectors in `cpp/update_flow_core/tests/config-equivalence-vectors.json` +(contract C1, shared with the JS/Harmony bun test and the Android JUnit test) +against the extracted `PushyConfigsEquivalent`, in both argument orders. A real GCD worker captures the request generation and blocks at a controlled response boundary. Semaphores establish each ordering. Reset races are exercised diff --git a/scripts/tests/ios-purge-restore/purge_restore_test.mm b/scripts/tests/ios-purge-restore/purge_restore_test.mm index d1971a4a..ed9fc2fd 100644 --- a/scripts/tests/ios-purge-restore/purge_restore_test.mm +++ b/scripts/tests/ios-purge-restore/purge_restore_test.mm @@ -512,29 +512,28 @@ static void DefaultsIsolation(void) { // Canonical config equality shared with JS/Android/Harmony: equal canonical // forms are the same config (persistConfiguration no-op, hasJsRound match). +// Replays the cross-platform vectors every implementation consumes; the file +// is located from this source file (/scripts/tests/ios-purge-restore/). static void ConfigEquivalence(void) { - struct Pair { NSString *a, *b; bool equivalent; const char *why; }; - const Pair pairs[] = { - {@"{\"appKey\":\"k\",\"endpoints\":[\"https://a/\",\"https://a\"]}", - @"{\"endpoints\":[\"https://a\"],\"appKey\":\"k\",\"disabled\":false,\"queryUrls\":[]," - @"\"rnu\":\"\",\"rn\":\"\",\"afterDownload\":\"none\"}", - true, "defaults, key order, trailing slash and duplicates are not changes"}, - {@"{\"appKey\":\"k\",\"queryUrls\":[\"https://q//\"]}", - @"{\"appKey\":\"k\",\"queryUrls\":[\"https://q\",\"https://q/\"]}", - true, "every trailing slash is stripped before deduplication"}, - {@"{\"appKey\":\"k\"}", @"{\"appKey\":\"j\"}", false, "a different appKey is a change"}, - {@"{\"appKey\":\"k\",\"disabled\":0}", @"{\"appKey\":\"k\"}", false, "0 is not false"}, - {@"{\"appKey\":\"k\",\"endpoints\":[\"https://a\",\"https://b\"]}", - @"{\"appKey\":\"k\",\"endpoints\":[\"https://b\",\"https://a\"]}", - false, "endpoint order is significant"}, - {@"[1]", @"[1]", false, "a non-object is never equivalent"}, - {@"{", @"{", false, "unparseable text is never equivalent, even to itself"}, - }; - for (const auto &pair : pairs) { - Expect(PushyConfigsEquivalent(pair.a, pair.b) == pair.equivalent, pair.why); - Expect(PushyConfigsEquivalent(pair.b, pair.a) == pair.equivalent, "equivalence is symmetric"); + NSString *repo = [[[[@__FILE__ stringByDeletingLastPathComponent] + stringByDeletingLastPathComponent] stringByDeletingLastPathComponent] + stringByDeletingLastPathComponent]; + NSString *path = [repo stringByAppendingPathComponent: + @"cpp/update_flow_core/tests/config-equivalence-vectors.json"]; + NSData *data = [NSData dataWithContentsOfFile:path]; + Expect(data != nil, "config-equivalence-vectors.json is readable"); + NSDictionary *file = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; + NSArray *cases = [file isKindOfClass:NSDictionary.class] ? file[@"cases"] : nil; + Expect([cases isKindOfClass:NSArray.class] && cases.count >= 30, "vector file has cases"); + for (NSDictionary *vector in cases) { + NSString *a = vector[@"a"], *b = vector[@"b"]; + bool equivalent = [vector[@"equivalent"] boolValue]; + std::string why = std::string("vector \"") + [vector[@"name"] UTF8String] + "\""; + Expect(PushyConfigsEquivalent(a, b) == equivalent, why.c_str()); + Expect(PushyConfigsEquivalent(b, a) == equivalent, (why + " (reversed)").c_str()); } Expect(!PushyConfigsEquivalent(nil, @"{\"appKey\":\"k\"}"), "no stored config is a change"); + std::printf("[PASS] config_equivalence: %lu shared vectors\n", (unsigned long)cases.count); } int main(int argc, char **argv) { @@ -557,7 +556,6 @@ int main(int argc, char **argv) { std::printf("[PASS] defaults_isolation\n"); testName = "config_equivalence"; ConfigEquivalence(); - std::printf("[PASS] config_equivalence\n"); struct Case { const char *name; void (*run)(); }; const Case cases[] = { {"late_commit", LateCommit}, diff --git a/src/__tests__/configEquivalenceVectors.test.ts b/src/__tests__/configEquivalenceVectors.test.ts new file mode 100644 index 00000000..d8e2a141 --- /dev/null +++ b/src/__tests__/configEquivalenceVectors.test.ts @@ -0,0 +1,62 @@ +import { describe, expect, test } from 'bun:test'; +import { readFileSync } from 'node:fs'; +import { URL } from 'node:url'; +import { runInNewContext } from 'node:vm'; +import vectorsFile from '../../cpp/update_flow_core/tests/config-equivalence-vectors.json'; +import { nativeConfigsEquivalent } from '../updateFlowCore'; + +// Contract C1 (native config equivalence) is implemented four times: here, +// Harmony UpdateContext.ts, Android ConfigEquivalence.java and iOS +// PushyConfigsEquivalent. All four replay the same committed vectors; this +// file covers the two TypeScript ones, ConfigEquivalenceTest (JUnit) and +// scripts/test-ios-purge-restore.sh the natives. +interface Vector { + name: string; + a: string; + b: string; + equivalent: boolean; +} +const vectors = vectorsFile.cases as Vector[]; + +// UpdateContext.ts imports @ohos modules, so it is evaluated with its imports +// stripped (as in nativeConfiguration.test.ts); the C1 functions only depend +// on the file's own source. +function harmonyEquivalence(): (a: string, b: string) => boolean { + const source = readFileSync( + new URL( + '../../harmony/pushy/src/main/ets/UpdateContext.ts', + import.meta.url + ), + 'utf8' + ) + .replace(/^import[\s\S]*?;\r?\n/gm, '') + .replace(/^export\s*\{[^}]*\}(?:\s*from\s*[^;]*)?;\r?\n/gm, '') + .replace(/^export /gm, ''); + const js = new Bun.Transpiler({ loader: 'ts' }).transformSync(source); + return runInNewContext(`${js}\nnativeConfigsEquivalent;`, {}); +} + +const implementations: Array<[string, (a: string, b: string) => boolean]> = [ + ['js', nativeConfigsEquivalent], + ['harmony', harmonyEquivalence()], +]; + +describe('C1 config equivalence vectors', () => { + test('vector file is well formed', () => { + expect(vectors.length).toBeGreaterThanOrEqual(30); + expect(new Set(vectors.map((v) => v.name)).size).toBe(vectors.length); + expect(vectors.some((v) => v.equivalent)).toBe(true); + expect(vectors.some((v) => !v.equivalent)).toBe(true); + }); + + for (const [platform, equivalent] of implementations) { + describe(platform, () => { + for (const vector of vectors) { + test(vector.name, () => { + expect(equivalent(vector.a, vector.b)).toBe(vector.equivalent); + expect(equivalent(vector.b, vector.a)).toBe(vector.equivalent); + }); + } + }); + } +}); diff --git a/src/updateFlowCore.ts b/src/updateFlowCore.ts index 482d3073..05b645e9 100644 --- a/src/updateFlowCore.ts +++ b/src/updateFlowCore.ts @@ -127,6 +127,34 @@ const NATIVE_CONFIG_DEFAULTS: Record = { afterDownload: 'none', }; +const jsonDeepEqual = (a: unknown, b: unknown): boolean => { + if (a === b) { + return true; + } + if (Array.isArray(a) || Array.isArray(b)) { + return ( + Array.isArray(a) && + Array.isArray(b) && + a.length === b.length && + a.every((value, index) => jsonDeepEqual(value, b[index])) + ); + } + if (!a || !b || typeof a !== 'object' || typeof b !== 'object') { + return false; + } + const left = a as Record; + const right = b as Record; + const leftKeys = Object.keys(left); + return ( + leftKeys.length === Object.keys(right).length && + leftKeys.every( + (key) => Object.hasOwn(right, key) && jsonDeepEqual(left[key], right[key]) + ) + ); +}; + +// Each string loses every trailing '/', then entries (strings or not) are +// deduplicated by JSON value, keeping the first occurrence. const canonicalUrlList = (value: unknown): unknown => { if (!Array.isArray(value)) { return value; @@ -134,14 +162,14 @@ const canonicalUrlList = (value: unknown): unknown => { const result: unknown[] = []; const visited = new Set(); for (const item of value) { - if (typeof item !== 'string') { + if (typeof item === 'string') { + const url = item.replace(/\/+$/, ''); + if (!visited.has(url)) { + visited.add(url); + result.push(url); + } + } else if (!result.some((existing) => jsonDeepEqual(existing, item))) { result.push(item); - continue; - } - const url = item.replace(/\/+$/, ''); - if (!visited.has(url)) { - visited.add(url); - result.push(url); } } return result; @@ -177,32 +205,6 @@ export function canonicalizeNativeConfig( return canonical; } -const jsonDeepEqual = (a: unknown, b: unknown): boolean => { - if (a === b) { - return true; - } - if (Array.isArray(a) || Array.isArray(b)) { - return ( - Array.isArray(a) && - Array.isArray(b) && - a.length === b.length && - a.every((value, index) => jsonDeepEqual(value, b[index])) - ); - } - if (!a || !b || typeof a !== 'object' || typeof b !== 'object') { - return false; - } - const left = a as Record; - const right = b as Record; - const leftKeys = Object.keys(left); - return ( - leftKeys.length === Object.keys(right).length && - leftKeys.every( - (key) => Object.hasOwn(right, key) && jsonDeepEqual(left[key], right[key]) - ) - ); -}; - /** * Whether two native check config JSON strings describe the same config * (contract C1): their canonical forms are deep-equal, key order ignored. From 3ce96c77fee7d3f280768da6967c4901c67d6f01 Mon Sep 17 00:00:00 2001 From: sunnylqm Date: Mon, 12 Oct 2026 12:18:38 +0800 Subject: [PATCH 21/21] test(e2e): record the bundleHash of every checkUpdate, not the last non-empty one Co-Authored-By: Claude Opus 5.5 --- Example/e2etest/scripts/local-e2e-server.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Example/e2etest/scripts/local-e2e-server.ts b/Example/e2etest/scripts/local-e2e-server.ts index 21ec09b5..a7119769 100644 --- a/Example/e2etest/scripts/local-e2e-server.ts +++ b/Example/e2etest/scripts/local-e2e-server.ts @@ -70,7 +70,7 @@ function safeResolve(urlPath: string) { return target; } -// Most recent non-empty bundleHash each platform sent with checkUpdate, so a +// bundleHash each platform sent with its most recent checkUpdate, so a // test can compare it against the sha256 of the bundle embedded in the binary. const lastBundleHash: Record = {}; @@ -215,9 +215,10 @@ const server = Bun.serve({ diffV?: unknown; bundleHash?: unknown; }; - if (typeof payload.bundleHash === 'string' && payload.bundleHash) { - lastBundleHash[platform] = payload.bundleHash; - } + // Every request overwrites: a later check that drops the hash must + // fail the identity test rather than hide behind an earlier one. + lastBundleHash[platform] = + typeof payload.bundleHash === 'string' ? payload.bundleHash : ''; const currentHash = typeof payload.hash === 'string' ? payload.hash : ''; const diffV = typeof payload.diffV === 'number' ? payload.diffV : 0;