From 5f5c585e68ffe580065fc9f4349a5b4384c442d1 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Wed, 12 Aug 2026 00:23:22 +0200 Subject: [PATCH] fix(app): avoid unsupported Android array sorting (#3241) Hermes in the production Android app does not provide Array.prototype.toSorted. Multi-host project selection and provider cache eviction could therefore crash when they reached deterministic sorting paths. --- .../app/src/data/provider-snapshot-cache.ts | 4 +-- .../app/src/projects/host-project-model.ts | 2 +- .../app/src/projects/host-projects.test.ts | 26 +++++++++++++++++++ 3 files changed, 29 insertions(+), 3 deletions(-) diff --git a/packages/app/src/data/provider-snapshot-cache.ts b/packages/app/src/data/provider-snapshot-cache.ts index 43b015661..d252613d1 100644 --- a/packages/app/src/data/provider-snapshot-cache.ts +++ b/packages/app/src/data/provider-snapshot-cache.ts @@ -136,7 +136,7 @@ export function createProviderSnapshotCache( } let totalBytes = entries.reduce((total, entry) => total + entry.bytes, 0); - const retainedEntries = entries.toSorted(oldestFirst); + const retainedEntries = [...entries].sort(oldestFirst); while (totalBytes > maxBytes) { const evicted = retainedEntries.shift(); if (!evicted) break; @@ -186,7 +186,7 @@ export function createProviderSnapshotCache( if (canStoreIncoming) projectedBytes += incomingBytes; const keysToRemove: string[] = []; - for (const candidate of retainedEntries.toSorted(oldestFirst)) { + for (const candidate of [...retainedEntries].sort(oldestFirst)) { if (projectedBytes <= maxBytes) break; projectedBytes -= candidate.bytes; keysToRemove.push(candidate.key); diff --git a/packages/app/src/projects/host-project-model.ts b/packages/app/src/projects/host-project-model.ts index 22f185238..fb90ebff9 100644 --- a/packages/app/src/projects/host-project-model.ts +++ b/packages/app/src/projects/host-project-model.ts @@ -194,7 +194,7 @@ export function resolveEquivalentHostProjectCandidate(input: { (project) => project.projectKey === input.candidate.projectKey, ); if (equivalents.length === 0) return null; - return equivalents.toSorted((left, right) => + return equivalents.sort((left, right) => compareHostProjectsForServer(left, right, input.serverId), )[0]!; } diff --git a/packages/app/src/projects/host-projects.test.ts b/packages/app/src/projects/host-projects.test.ts index 798d08b08..2806f5b90 100644 --- a/packages/app/src/projects/host-projects.test.ts +++ b/packages/app/src/projects/host-projects.test.ts @@ -7,6 +7,7 @@ import { getWorktreeSupportForHostProject, hostProjectFromRoute, hostProjectFromWorkspace, + resolveEquivalentHostProjectCandidate, } from "./host-project-model"; import { normalizeWorkspaceDescriptor } from "@/stores/session-store"; @@ -36,6 +37,31 @@ function project(): HostProjectListItem { } describe("host project lookups", () => { + test("resolves equivalent projects without Array.prototype.toSorted", () => { + const descriptor = Object.getOwnPropertyDescriptor(Array.prototype, "toSorted"); + Reflect.deleteProperty(Array.prototype, "toSorted"); + const first = project(); + first.viewKey = "view:first"; + first.projectName = "First"; + const second = project(); + second.viewKey = "view:second"; + second.projectName = "Second"; + const projects = [second, first]; + + try { + expect( + resolveEquivalentHostProjectCandidate({ + candidate: first, + projects, + serverId: "host-a", + }), + ).toBe(first); + expect(projects).toEqual([second, first]); + } finally { + if (descriptor) Reflect.defineProperty(Array.prototype, "toSorted", descriptor); + } + }); + test("returns host-local ids and roots without falling back to the grouping key", () => { expect(getHostProjectId(project(), "host-b")).toBe("prj_b"); expect(getHostProjectSourceDirectory(project(), "host-b")).toBe("/repo/b");