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.
This commit is contained in:
Mohamed Boudra
2026-08-12 00:23:22 +02:00
committed by GitHub
parent 23d7a8eaf9
commit 5f5c585e68
3 changed files with 29 additions and 3 deletions
@@ -136,7 +136,7 @@ export function createProviderSnapshotCache(
} }
let totalBytes = entries.reduce((total, entry) => total + entry.bytes, 0); let totalBytes = entries.reduce((total, entry) => total + entry.bytes, 0);
const retainedEntries = entries.toSorted(oldestFirst); const retainedEntries = [...entries].sort(oldestFirst);
while (totalBytes > maxBytes) { while (totalBytes > maxBytes) {
const evicted = retainedEntries.shift(); const evicted = retainedEntries.shift();
if (!evicted) break; if (!evicted) break;
@@ -186,7 +186,7 @@ export function createProviderSnapshotCache(
if (canStoreIncoming) projectedBytes += incomingBytes; if (canStoreIncoming) projectedBytes += incomingBytes;
const keysToRemove: string[] = []; const keysToRemove: string[] = [];
for (const candidate of retainedEntries.toSorted(oldestFirst)) { for (const candidate of [...retainedEntries].sort(oldestFirst)) {
if (projectedBytes <= maxBytes) break; if (projectedBytes <= maxBytes) break;
projectedBytes -= candidate.bytes; projectedBytes -= candidate.bytes;
keysToRemove.push(candidate.key); keysToRemove.push(candidate.key);
@@ -194,7 +194,7 @@ export function resolveEquivalentHostProjectCandidate(input: {
(project) => project.projectKey === input.candidate.projectKey, (project) => project.projectKey === input.candidate.projectKey,
); );
if (equivalents.length === 0) return null; if (equivalents.length === 0) return null;
return equivalents.toSorted((left, right) => return equivalents.sort((left, right) =>
compareHostProjectsForServer(left, right, input.serverId), compareHostProjectsForServer(left, right, input.serverId),
)[0]!; )[0]!;
} }
@@ -7,6 +7,7 @@ import {
getWorktreeSupportForHostProject, getWorktreeSupportForHostProject,
hostProjectFromRoute, hostProjectFromRoute,
hostProjectFromWorkspace, hostProjectFromWorkspace,
resolveEquivalentHostProjectCandidate,
} from "./host-project-model"; } from "./host-project-model";
import { normalizeWorkspaceDescriptor } from "@/stores/session-store"; import { normalizeWorkspaceDescriptor } from "@/stores/session-store";
@@ -36,6 +37,31 @@ function project(): HostProjectListItem {
} }
describe("host project lookups", () => { 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", () => { test("returns host-local ids and roots without falling back to the grouping key", () => {
expect(getHostProjectId(project(), "host-b")).toBe("prj_b"); expect(getHostProjectId(project(), "host-b")).toBe("prj_b");
expect(getHostProjectSourceDirectory(project(), "host-b")).toBe("/repo/b"); expect(getHostProjectSourceDirectory(project(), "host-b")).toBe("/repo/b");