diff --git a/bun.lock b/bun.lock index 0bd6d3deaa..f44103e550 100644 --- a/bun.lock +++ b/bun.lock @@ -339,7 +339,7 @@ "@hyperframes/studio-server": "workspace:*", "@mcp-b/global": "^5.0.1", "@phosphor-icons/react": "^2.1.10", - "@tanstack/react-virtual": "^3.14.6", + "@tanstack/react-virtual": "^3.14.13", "bpm-detective": "^2.0.5", "cn": "0.2.4", "dockview-react": "8.3.1", @@ -1170,9 +1170,9 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], - "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.6", "", { "dependencies": { "@tanstack/virtual-core": "3.17.4" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-4+Uq8m0/gzO4kMCHUEpTtGX1RnONK0C+g88b2ltwPMWUBiaVarBuWKoPJaz7gj1cKCVRAdyu+U8GcKhwCc2beA=="], + "@tanstack/react-virtual": ["@tanstack/react-virtual@3.14.13", "", { "dependencies": { "@tanstack/virtual-core": "3.17.11" }, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-JbDTAwtzZ99aOeCrAfW5EsE5KSq5RWh6Af2dtFwyLIIk48Ja7vm6n4axu/43T3vfjFnEGJalxJ1wyYjSQD6bSg=="], - "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.4", "", {}, "sha512-nGm5KteqxasUdThLc2izl6dHUqLv0LQj7Nuyo5gYalTPf/U8a9ermvsl7reT+6ioBW1l8WfpP/mcU338nLXpqw=="], + "@tanstack/virtual-core": ["@tanstack/virtual-core@3.17.11", "", {}, "sha512-+ILjvtHup6Y2hzQ6YzwMgX1Q+oQpxEGOXCEsCNaPoIP0VxMbizIBTmYTDtkerkIQS8/CbP1BRuyt8V/8BCsy1g=="], "@tootallnate/once": ["@tootallnate/once@2.0.1", "", {}, "sha512-HqmEUIGRJ5fSXchkVgR5F7qn48bDBzv0kWj/Kfu5e6uci4UlEeng4331LnBkWffb++Ei3FOVLxo8JJWMFBDMeQ=="], diff --git a/packages/studio-server/src/history/pruneHistories.test.ts b/packages/studio-server/src/history/pruneHistories.test.ts index ccd83cce21..47705c0054 100644 --- a/packages/studio-server/src/history/pruneHistories.test.ts +++ b/packages/studio-server/src/history/pruneHistories.test.ts @@ -31,6 +31,8 @@ function tempDir(prefix: string): string { } const DAY_MS = 24 * 60 * 60 * 1000; +// Date.now() rounds down to the millisecond; a file written in that millisecond can carry a later mtime. +const pastKeepWindow = () => Date.now() + 1 + KEEP_GONE_PROJECT_HISTORY_MS; async function projectWithHistory( historyRoot: string, @@ -105,7 +107,7 @@ describe("pruneGoneProjectHistories", () => { expect(await prunedIds(historyRoot, { ...elsewhere, now: Date.now() + 13 * DAY_MS })).toEqual( [], ); - expect(await prunedIds(historyRoot, { ...elsewhere, now: Date.now() + 14 * DAY_MS })).toEqual([ + expect(await prunedIds(historyRoot, { ...elsewhere, now: pastKeepWindow() })).toEqual([ gone.id, ]); }); @@ -178,7 +180,7 @@ describe("pruneGoneProjectHistories", () => { chmodSync(locked, 0o000); try { - const later = Date.now() + KEEP_GONE_PROJECT_HISTORY_MS; + const later = pastKeepWindow(); const onError = (error: unknown) => errors.push(error); expect(await prunedIds(historyRoot, { now: later, onError })).toEqual([gone.id]); } finally { @@ -206,7 +208,7 @@ describe("pruneGoneProjectHistories", () => { rmSync(join(mountPoint.projectDir, ".hyperframes"), { recursive: true, force: true }); rmSync(dirname(deleted.projectDir), { recursive: true, force: true }); - const later = Date.now() + KEEP_GONE_PROJECT_HISTORY_MS; + const later = pastKeepWindow(); expect(await prunedIds(historyRoot, { tempDir: historyRoot, now: later })).toEqual([ deleted.id, ]); @@ -227,7 +229,7 @@ describe("pruneGoneProjectHistories", () => { rmSync(folderGone.projectDir, { recursive: true, force: true }); rmSync(dirname(parentGone.projectDir), { recursive: true, force: true }); - const later = Date.now() + KEEP_GONE_PROJECT_HISTORY_MS; + const later = pastKeepWindow(); expect(await prunedIds(historyRoot, { tempDir: historyRoot, now: later })).toEqual([ emptied.id, ]); diff --git a/packages/studio/package.json b/packages/studio/package.json index 607909fa4d..597d4af3bb 100644 --- a/packages/studio/package.json +++ b/packages/studio/package.json @@ -103,7 +103,7 @@ "@hyperframes/studio-server": "workspace:*", "@mcp-b/global": "^5.0.1", "@phosphor-icons/react": "^2.1.10", - "@tanstack/react-virtual": "^3.14.6", + "@tanstack/react-virtual": "^3.14.13", "bpm-detective": "^2.0.5", "cn": "0.2.4", "dockview-react": "8.3.1", diff --git a/packages/studio/src/player/components/useTimelineVirtualRows.test.tsx b/packages/studio/src/player/components/useTimelineVirtualRows.test.tsx index c0be0a8ea5..e4d8256cef 100644 --- a/packages/studio/src/player/components/useTimelineVirtualRows.test.tsx +++ b/packages/studio/src/player/components/useTimelineVirtualRows.test.tsx @@ -2,7 +2,13 @@ import React, { act } from "react"; import { createRoot } from "react-dom/client"; -import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { + elementScroll, + observeElementOffset, + observeElementRect, + Virtualizer, +} from "@tanstack/react-virtual"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { createTimelineRowGeometry, RULER_H, TRACKS_TOP_PAD } from "./timelineLayout"; import { extractTimelineVirtualRowRange, useTimelineVirtualRows } from "./useTimelineVirtualRows"; import type { TimelineScrollViewportSnapshot } from "./useTimelineScrollViewport"; @@ -203,4 +209,27 @@ describe("useTimelineVirtualRows", () => { expect(rows.some((row) => row.index === 500)).toBe(false); act(() => root.unmount()); }); + + // A scroll-end timer left running fires into the torn-down test window ("window is not defined"). + it("gets no virtualizer change after unmount from a scroll just before it", async () => { + const scroll = createScrollElement(0); + const onChange = vi.fn(); + const virtualizer = new Virtualizer({ + count: 100, + getScrollElement: () => scroll, + estimateSize: () => 48, + scrollToFn: elementScroll, + observeElementRect, + observeElementOffset, + onChange, + }); + const unmount = virtualizer._didMount(); + virtualizer._willUpdate(); + scroll.dispatchEvent(new Event("scroll")); + unmount(); + onChange.mockClear(); + + await new Promise((settle) => setTimeout(settle, 200)); + expect(onChange).not.toHaveBeenCalled(); + }); });