authorgravatar for 269035359+t3-code[bot]@users.noreply.github.comt3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> 2026-08-18 17:22:37-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-18 17:22:37-07:00
logfe281c5408164250da6688de6fc71ac12d8a9d8f
tree2323b64420a0971700549543003e7dce1f15b7e4
parentbcfd485869a5eb9e97c6d281ac839c1cd7c1bd99
signature Signed by PGP key B5690EEEBB952194

fix(desktop): throttle hidden preview rendering (#7445)

Co-authored-by: t3-code[bot] <269035359+t3-code[bot]@users.noreply.github.com> Co-authored-by: maria <254055478+maria-rcks@users.noreply.github.com>

4 files changed, 327 insertions(+), 30 deletions(-)

apps/desktop/src/preview/Manager.test.ts+225
...@@ -1797,6 +1797,216 @@ describe("PreviewManager", () => {...@@ -1797,6 +1797,216 @@ describe("PreviewManager", () => {
1797 ),1797 ),
1798 );1798 );
17991799
1800 effectIt.effect("keeps window unthrottled until the final frame capture stops", () =>
1801 withManager((manager) =>
1802 Effect.gen(function* () {
1803 const setBackgroundThrottling = vi.fn();
1804 const capturePage = vi.fn(async () => ({
1805 toJPEG: () => Buffer.from("recording-frame"),
1806 getSize: () => ({ width: 1280, height: 720 }),
1807 }));
1808 const webContentsById = new Map([
1809 [41, makeTestPreviewWebContents(capturePage, 41)],
1810 [42, makeTestPreviewWebContents(capturePage, 42)],
1811 ]);
1812 fromId.mockImplementation((id) =>
1813 id === undefined ? null : (webContentsById.get(id) ?? null),
1814 );
1815
1816 yield* manager.createTab("tab_capture_throttling_1");
1817 yield* manager.createTab("tab_capture_throttling_2");
1818 yield* manager.registerWebview("tab_capture_throttling_1", 41);
1819 yield* manager.registerWebview("tab_capture_throttling_2", 42);
1820 yield* manager.setMainWindow({
1821 isDestroyed: () => false,
1822 once: vi.fn(),
1823 webContents: { setBackgroundThrottling },
1824 } as never);
1825
1826 yield* manager.startRecording("tab_capture_throttling_1");
1827 yield* manager.startRecording("tab_capture_throttling_2");
1828 expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
1829
1830 yield* manager.stopRecording("tab_capture_throttling_1");
1831 expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
1832
1833 yield* manager.stopRecording("tab_capture_throttling_2");
1834 expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true]]);
1835 }),
1836 ),
1837 );
1838
1839 effectIt.effect("does not commit failed starts and retries throttle restoration", () =>
1840 withManager((manager) =>
1841 Effect.gen(function* () {
1842 const setBackgroundThrottling = vi.fn<(enabled: boolean) => void>();
1843 const capturePage = vi.fn(async () => ({
1844 toJPEG: () => Buffer.from("recording-frame"),
1845 getSize: () => ({ width: 1280, height: 720 }),
1846 }));
1847 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
1848
1849 yield* manager.createTab("tab_capture_throttling_failure");
1850 yield* manager.registerWebview("tab_capture_throttling_failure", 42);
1851 yield* manager.setMainWindow({
1852 isDestroyed: () => false,
1853 once: vi.fn(),
1854 webContents: { setBackgroundThrottling },
1855 } as never);
1856
1857 setBackgroundThrottling.mockImplementationOnce(() => {
1858 throw new Error("start throttling update failed");
1859 });
1860 const failedStart = yield* Effect.exit(
1861 manager.startRecording("tab_capture_throttling_failure"),
1862 );
1863 expect(Exit.isFailure(failedStart)).toBe(true);
1864
1865 yield* manager.startRecording("tab_capture_throttling_failure");
1866 expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false]]);
1867
1868 setBackgroundThrottling.mockImplementationOnce(() => {
1869 throw new Error("stop throttling update failed");
1870 });
1871 yield* manager.stopRecording("tab_capture_throttling_failure");
1872 expect(setBackgroundThrottling.mock.calls).toEqual([[false], [false], [true], [true]]);
1873
1874 yield* manager.startRecording("tab_capture_throttling_failure");
1875 yield* manager.stopRecording("tab_capture_throttling_failure");
1876 expect(setBackgroundThrottling.mock.calls).toEqual([
1877 [false],
1878 [false],
1879 [true],
1880 [true],
1881 [false],
1882 [true],
1883 ]);
1884 }),
1885 ),
1886 );
1887
1888 effectIt.effect("does not publish a replacement window when capture reconciliation fails", () =>
1889 withManager((manager) =>
1890 Effect.gen(function* () {
1891 const setBackgroundThrottling = vi.fn(() => {
1892 throw new Error("replacement throttling update failed");
1893 });
1894 const capturePage = vi.fn(async () => ({
1895 toJPEG: () => Buffer.from("recording-frame"),
1896 getSize: () => ({ width: 1280, height: 720 }),
1897 }));
1898 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
1899
1900 yield* manager.createTab("tab_capture_replacement_failure");
1901 yield* manager.registerWebview("tab_capture_replacement_failure", 42);
1902 yield* manager.startRecording("tab_capture_replacement_failure");
1903
1904 const failedReplacement = yield* Effect.exit(
1905 manager.setMainWindow({
1906 isDestroyed: () => false,
1907 once: vi.fn(),
1908 webContents: { setBackgroundThrottling },
1909 } as never),
1910 );
1911 expect(Exit.isFailure(failedReplacement)).toBe(true);
1912
1913 yield* manager.stopRecording("tab_capture_replacement_failure");
1914 expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
1915 }),
1916 ),
1917 );
1918
1919 effectIt.effect("ignores close events from replaced main windows", () =>
1920 withManager((manager) =>
1921 Effect.gen(function* () {
1922 let closeFirstWindow: (() => void) | undefined;
1923 const firstWindowThrottling = vi.fn();
1924 const replacementWindowThrottling = vi.fn();
1925 const capturePage = vi.fn(async () => ({
1926 toJPEG: () => Buffer.from("recording-frame"),
1927 getSize: () => ({ width: 1280, height: 720 }),
1928 }));
1929 fromId.mockReturnValue(makeTestPreviewWebContents(capturePage));
1930
1931 yield* manager.createTab("tab_replaced_window_close");
1932 yield* manager.registerWebview("tab_replaced_window_close", 42);
1933 yield* manager.setMainWindow({
1934 isDestroyed: () => false,
1935 once: vi.fn((event: string, listener: () => void) => {
1936 if (event === "closed") closeFirstWindow = listener;
1937 }),
1938 webContents: { setBackgroundThrottling: firstWindowThrottling },
1939 } as never);
1940 yield* manager.setMainWindow({
1941 isDestroyed: () => false,
1942 once: vi.fn(),
1943 webContents: { setBackgroundThrottling: replacementWindowThrottling },
1944 } as never);
1945
1946 closeFirstWindow?.();
1947 yield* manager.startRecording("tab_replaced_window_close");
1948 expect(firstWindowThrottling).not.toHaveBeenCalled();
1949 expect(replacementWindowThrottling.mock.calls).toEqual([[false]]);
1950 yield* manager.stopRecording("tab_replaced_window_close");
1951 expect(replacementWindowThrottling.mock.calls).toEqual([[false], [true]]);
1952 }),
1953 ),
1954 );
1955
1956 effectIt.effect("releases frame capture when the main window closes", () =>
1957 withManager((manager) =>
1958 Effect.gen(function* () {
1959 let closeMainWindow: (() => void) | undefined;
1960 const firstWindowThrottling = vi.fn();
1961 const replacementWindowThrottling = vi.fn();
1962 const capturePage = vi.fn(async () => ({
1963 toJPEG: () => Buffer.from("recording-frame"),
1964 getSize: () => ({ width: 1280, height: 720 }),
1965 }));
1966 const webContentsById = new Map([
1967 [42, makeTestPreviewWebContents(capturePage, 42)],
1968 [43, makeTestPreviewWebContents(capturePage, 43)],
1969 ]);
1970 fromId.mockImplementation((id) =>
1971 id === undefined ? null : (webContentsById.get(id) ?? null),
1972 );
1973
1974 yield* manager.createTab("tab_window_close_recording");
1975 yield* manager.createTab("tab_window_close_race");
1976 yield* manager.registerWebview("tab_window_close_recording", 42);
1977 yield* manager.registerWebview("tab_window_close_race", 43);
1978 yield* manager.setMainWindow({
1979 isDestroyed: () => false,
1980 once: vi.fn((event: string, listener: () => void) => {
1981 if (event === "closed") closeMainWindow = listener;
1982 }),
1983 webContents: { setBackgroundThrottling: firstWindowThrottling },
1984 } as never);
1985 yield* manager.startRecording("tab_window_close_recording");
1986 expect(firstWindowThrottling.mock.calls).toEqual([[false]]);
1987
1988 closeMainWindow?.();
1989 const racedStart = yield* Effect.exit(manager.startRecording("tab_window_close_race"));
1990 expect(Exit.isFailure(racedStart)).toBe(true);
1991 if (Exit.isFailure(racedStart)) {
1992 expect(Option.getOrThrow(Cause.findErrorOption(racedStart.cause))).toMatchObject({
1993 _tag: "PreviewMainWindowClosedError",
1994 tabId: "tab_window_close_race",
1995 });
1996 }
1997 yield* Effect.yieldNow;
1998 yield* Effect.yieldNow;
1999
2000 yield* manager.setMainWindow({
2001 isDestroyed: () => false,
2002 once: vi.fn(),
2003 webContents: { setBackgroundThrottling: replacementWindowThrottling },
2004 } as never);
2005 expect(replacementWindowThrottling).not.toHaveBeenCalled();
2006 }),
2007 ),
2008 );
2009
1800 effectIt.effect("captures hidden preview recordings independently for concurrent tabs", () =>2010 effectIt.effect("captures hidden preview recordings independently for concurrent tabs", () =>
1801 withManager((manager) =>2011 withManager((manager) =>
1802 Effect.gen(function* () {2012 Effect.gen(function* () {
...@@ -2106,6 +2316,8 @@ describe("PreviewManager", () => {...@@ -2106,6 +2316,8 @@ describe("PreviewManager", () => {
2106 effectIt.effect("shares background frame capture between recording and picture-in-picture", () =>2316 effectIt.effect("shares background frame capture between recording and picture-in-picture", () =>
2107 withManager((manager) =>2317 withManager((manager) =>
2108 Effect.gen(function* () {2318 Effect.gen(function* () {
2319 const setBackgroundThrottling = vi.fn();
2320 const mainWindowWebContents = { setBackgroundThrottling };
2109 const jpeg = Buffer.from("shared-preview-frame");2321 const jpeg = Buffer.from("shared-preview-frame");
2110 const capturePage = vi.fn(async () => ({2322 const capturePage = vi.fn(async () => ({
2111 toJPEG: () => jpeg,2323 toJPEG: () => jpeg,
...@@ -2113,6 +2325,7 @@ describe("PreviewManager", () => {...@@ -2113,6 +2325,7 @@ describe("PreviewManager", () => {
2113 }));2325 }));
2114 fromId.mockReturnValue({2326 fromId.mockReturnValue({
2115 id: 42,2327 id: 42,
2328 hostWebContents: mainWindowWebContents,
2116 isDestroyed: () => false,2329 isDestroyed: () => false,
2117 getType: () => "webview",2330 getType: () => "webview",
2118 getURL: () => "https://example.com",2331 getURL: () => "https://example.com",
...@@ -2165,6 +2378,12 @@ describe("PreviewManager", () => {...@@ -2165,6 +2378,12 @@ describe("PreviewManager", () => {
2165 const states: PreviewManager.PreviewTabState[] = [];2378 const states: PreviewManager.PreviewTabState[] = [];
2166 const recordingFrames: DesktopPreviewRecordingFrame[] = [];2379 const recordingFrames: DesktopPreviewRecordingFrame[] = [];
21672380
2381 yield* manager.setMainWindow({
2382 isDestroyed: () => false,
2383 once: vi.fn(),
2384 webContents: mainWindowWebContents,
2385 } as never);
2386
2168 yield* manager.subscribeStateChanges((_tabId, state) =>2387 yield* manager.subscribeStateChanges((_tabId, state) =>
2169 Effect.sync(() => {2388 Effect.sync(() => {
2170 states.push(state);2389 states.push(state);
...@@ -2179,6 +2398,7 @@ describe("PreviewManager", () => {...@@ -2179,6 +2398,7 @@ describe("PreviewManager", () => {
2179 yield* manager.registerWebview("tab_pip", 42);2398 yield* manager.registerWebview("tab_pip", 42);
2180 yield* manager.openPictureInPicture("tab_pip");2399 yield* manager.openPictureInPicture("tab_pip");
21812400
2401 expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
2182 expect(browserWindowConstructor).toHaveBeenCalledWith(2402 expect(browserWindowConstructor).toHaveBeenCalledWith(
2183 expect.objectContaining({2403 expect.objectContaining({
2184 alwaysOnTop: true,2404 alwaysOnTop: true,
...@@ -2224,6 +2444,7 @@ describe("PreviewManager", () => {...@@ -2224,6 +2444,7 @@ describe("PreviewManager", () => {
2224 expect(recordingFrames).toHaveLength(1);2444 expect(recordingFrames).toHaveLength(1);
22252445
2226 yield* manager.stopRecording("tab_pip");2446 yield* manager.stopRecording("tab_pip");
2447 expect(setBackgroundThrottling.mock.calls).toEqual([[false]]);
2227 const framesBeforePictureInPictureOnlyTick = pictureInPictureSend.mock.calls.length;2448 const framesBeforePictureInPictureOnlyTick = pictureInPictureSend.mock.calls.length;
2228 yield* TestClock.adjust(100);2449 yield* TestClock.adjust(100);
2229 expect(capturePage).toHaveBeenCalledTimes(3);2450 expect(capturePage).toHaveBeenCalledTimes(3);
...@@ -2232,7 +2453,11 @@ describe("PreviewManager", () => {...@@ -2232,7 +2453,11 @@ describe("PreviewManager", () => {
2232 );2453 );
2233 expect(recordingFrames).toHaveLength(1);2454 expect(recordingFrames).toHaveLength(1);
22342455
2456 setBackgroundThrottling.mockImplementationOnce(() => {
2457 throw new Error("picture-in-picture throttling restore failed");
2458 });
2235 yield* manager.closePictureInPicture("tab_pip");2459 yield* manager.closePictureInPicture("tab_pip");
2460 expect(setBackgroundThrottling.mock.calls).toEqual([[false], [true], [true]]);
2236 expect(pictureInPictureWindow.close).toHaveBeenCalledOnce();2461 expect(pictureInPictureWindow.close).toHaveBeenCalledOnce();
2237 expect(states.at(-1)?.pictureInPicture).toBe(false);2462 expect(states.at(-1)?.pictureInPicture).toBe(false);
2238 const capturesAfterClose = capturePage.mock.calls.length;2463 const capturesAfterClose = capturePage.mock.calls.length;
apps/desktop/src/preview/Manager.ts+101-28
...@@ -526,6 +526,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -526,6 +526,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
526 const pictureInPictureAspectRatiosRef = yield* Ref.make<ReadonlyMap<string, number>>(new Map());526 const pictureInPictureAspectRatiosRef = yield* Ref.make<ReadonlyMap<string, number>>(new Map());
527 const pictureInPictureMutationSemaphore = yield* Semaphore.make(1);527 const pictureInPictureMutationSemaphore = yield* Semaphore.make(1);
528 const closingTabIdsRef = yield* Ref.make<ReadonlySet<string>>(new Set());528 const closingTabIdsRef = yield* Ref.make<ReadonlySet<string>>(new Set());
529 let frameCaptureWindowOpen = true;
530 let currentMainWindow: BrowserWindow | undefined;
531 let mainWindowCleanupFiber: Fiber.Fiber<void, never> | undefined;
529 const tabLifecycleLocks = new Map<532 const tabLifecycleLocks = new Map<
530 string,533 string,
531 { readonly semaphore: Semaphore.Semaphore; users: number }534 { readonly semaphore: Semaphore.Semaphore; users: number }
...@@ -583,35 +586,67 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -583,35 +586,67 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
583 ),586 ),
584 );587 );
585 });588 });
589 const setWindowBackgroundThrottling = Effect.fnUntraced(function* (
590 window: BrowserWindow,
591 enabled: boolean,
592 ) {
593 if (window.isDestroyed()) return;
594 yield* attempt({ operation: "frameCapture.setBackgroundThrottling" }, () =>
595 window.webContents.setBackgroundThrottling(enabled),
596 );
597 });
598 const setFrameCaptureBackgroundThrottling = Effect.fnUntraced(function* (enabled: boolean) {
599 const mainWindow = yield* Ref.get(mainWindowRef);
600 if (Option.isNone(mainWindow)) return;
601 yield* setWindowBackgroundThrottling(mainWindow.value, enabled);
602 });
586 const stopFrameCapture = Effect.fn("PreviewManager.stopFrameCapture")(function* (603 const stopFrameCapture = Effect.fn("PreviewManager.stopFrameCapture")(function* (
587 tabId: string,604 tabId: string,
588 consumer: FrameCaptureConsumer,605 consumer: FrameCaptureConsumer,
589 ) {606 ) {
590 const captureScope = yield* SynchronizedRef.modify(frameCaptureSessionsRef, (sessions) => {607 yield* SynchronizedRef.modifyEffect(frameCaptureSessionsRef, (sessions) =>
591 const current = sessions.get(tabId);608 Effect.gen(function* () {
592 if (!current || !current.consumers.has(consumer)) {609 const current = sessions.get(tabId);
593 return [undefined, sessions] as const;610 if (!current || !current.consumers.has(consumer)) {
594 }611 return [undefined, sessions] as const;
595 const consumers = new Set(current.consumers);612 }
596 consumers.delete(consumer);613 const consumers = new Set(current.consumers);
597 if (consumers.size > 0) {614 consumers.delete(consumer);
598 return [615 if (consumers.size > 0) {
599 undefined,616 return [
600 replaceMap(sessions, (copy) => {617 undefined,
601 copy.set(tabId, { ...current, consumers });618 replaceMap(sessions, (copy) => {
602 }),619 copy.set(tabId, { ...current, consumers });
603 ] as const;620 }),
604 }621 ] as const;
605 return [622 }
606 current.scope,623 const remainingSessions = replaceMap(sessions, (copy) => {
607 replaceMap(sessions, (copy) => {
608 copy.delete(tabId);624 copy.delete(tabId);
609 }),625 });
610 ] as const;626 if (remainingSessions.size === 0) {
627 yield* setFrameCaptureBackgroundThrottling(true).pipe(
628 Effect.retry({ times: 2 }),
629 Effect.catch((error) =>
630 Effect.logWarning("Failed to restore preview frame capture throttling.", { error }),
631 ),
632 );
633 }
634 return [current.scope, remainingSessions] as const;
635 }),
636 ).pipe(
637 Effect.flatMap((captureScope) =>
638 captureScope ? Scope.close(captureScope, Exit.void).pipe(Effect.ignore) : Effect.void,
639 ),
640 Effect.uninterruptible,
641 );
642 });
643
644 const stopAllRecordings = Effect.fn("PreviewManager.stopAllRecordings")(function* () {
645 const sessions = yield* SynchronizedRef.get(frameCaptureSessionsRef);
646 yield* Effect.forEach(sessions.keys(), (tabId) => stopFrameCapture(tabId, "recording"), {
647 concurrency: "unbounded",
648 discard: true,
611 });649 });
612 if (captureScope) {
613 yield* Scope.close(captureScope, Exit.void).pipe(Effect.ignore);
614 }
615 });650 });
616651
617 const deliverEvent = (652 const deliverEvent = (
...@@ -1691,10 +1726,32 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -1691,10 +1726,32 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
1691 const setMainWindow = Effect.fn("PreviewManager.setMainWindow")(function* (1726 const setMainWindow = Effect.fn("PreviewManager.setMainWindow")(function* (
1692 window: BrowserWindow,1727 window: BrowserWindow,
1693 ) {1728 ) {
1694 yield* Ref.set(mainWindowRef, Option.some(window));1729 if (mainWindowCleanupFiber) {
1695 window.once("closed", () => {1730 yield* Fiber.join(mainWindowCleanupFiber);
1696 runFork(closeAllPictureInPicture());1731 mainWindowCleanupFiber = undefined;
1697 });1732 }
1733 yield* SynchronizedRef.modifyEffect(frameCaptureSessionsRef, (sessions) =>
1734 Effect.gen(function* () {
1735 if (sessions.size > 0) {
1736 yield* setWindowBackgroundThrottling(window, false);
1737 }
1738 yield* Ref.set(mainWindowRef, Option.some(window));
1739 currentMainWindow = window;
1740 frameCaptureWindowOpen = true;
1741 window.once("closed", () => {
1742 if (currentMainWindow !== window) return;
1743 currentMainWindow = undefined;
1744 frameCaptureWindowOpen = false;
1745 mainWindowCleanupFiber = runFork(
1746 Effect.all([closeAllPictureInPicture(), stopAllRecordings()], {
1747 concurrency: "unbounded",
1748 discard: true,
1749 }).pipe(Effect.ignore),
1750 );
1751 });
1752 return [undefined, sessions] as const;
1753 }),
1754 ).pipe(Effect.uninterruptible);
1698 });1755 });
16991756
1700 const createTabUnlocked = Effect.fn("PreviewManager.createTabUnlocked")(function* (1757 const createTabUnlocked = Effect.fn("PreviewManager.createTabUnlocked")(function* (
...@@ -2597,6 +2654,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -2597,6 +2654,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
2597 );2654 );
2598 const created = yield* SynchronizedRef.modifyEffect(frameCaptureSessionsRef, (sessions) => {2655 const created = yield* SynchronizedRef.modifyEffect(frameCaptureSessionsRef, (sessions) => {
2599 return Effect.gen(function* () {2656 return Effect.gen(function* () {
2657 if (!frameCaptureWindowOpen) {
2658 return yield* new PreviewMainWindowClosedError({ tabId });
2659 }
2600 const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId);2660 const tab = (yield* SynchronizedRef.get(tabsRef)).get(tabId);
2601 if (!tab || (yield* Ref.get(closingTabIdsRef)).has(tabId)) {2661 if (!tab || (yield* Ref.get(closingTabIdsRef)).has(tabId)) {
2602 return yield* new PreviewTabNotFoundError({ tabId });2662 return yield* new PreviewTabNotFoundError({ tabId });
...@@ -2616,6 +2676,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -2616,6 +2676,9 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
2616 }),2676 }),
2617 ] as const;2677 ] as const;
2618 }2678 }
2679 if (sessions.size === 0) {
2680 yield* setFrameCaptureBackgroundThrottling(false);
2681 }
2619 const scope = yield* Scope.fork(parentScope, "sequential");2682 const scope = yield* Scope.fork(parentScope, "sequential");
2620 yield* Effect.forkIn(Effect.forever(captureNextFrame), scope);2683 yield* Effect.forkIn(Effect.forever(captureNextFrame), scope);
2621 return [2684 return [
...@@ -2628,7 +2691,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function...@@ -2628,7 +2691,7 @@ const makeNativeOperations = Effect.fn("PreviewManager.makeOperations")(function
2628 }),2691 }),
2629 ] as const;2692 ] as const;
2630 });2693 });
2631 });2694 }).pipe(Effect.uninterruptible);
2632 if (!created) return;2695 if (!created) return;
2633 yield* capturePreviewFrame(tabId).pipe(2696 yield* capturePreviewFrame(tabId).pipe(
2634 Effect.catch((error) =>2697 Effect.catch((error) =>
...@@ -3724,6 +3787,15 @@ export class PreviewWebviewNotInitializedError extends Schema.TaggedErrorClass<P...@@ -3724,6 +3787,15 @@ export class PreviewWebviewNotInitializedError extends Schema.TaggedErrorClass<P
3724 }3787 }
3725}3788}
37263789
3790export class PreviewMainWindowClosedError extends Schema.TaggedErrorClass<PreviewMainWindowClosedError>()(
3791 "PreviewMainWindowClosedError",
3792 { tabId: Schema.String },
3793) {
3794 override get message(): string {
3795 return `Cannot start preview frame capture while the main window is closed: ${this.tabId}`;
3796 }
3797}
3798
3727export class PreviewOperationError extends Schema.TaggedErrorClass<PreviewOperationError>()(3799export class PreviewOperationError extends Schema.TaggedErrorClass<PreviewOperationError>()(
3728 "PreviewOperationError",3800 "PreviewOperationError",
3729 {3801 {
...@@ -3930,6 +4002,7 @@ export const PreviewManagerError = Schema.Union([...@@ -3930,6 +4002,7 @@ export const PreviewManagerError = Schema.Union([
3930 PreviewTabNotFoundError,4002 PreviewTabNotFoundError,
3931 PreviewWebContentsNotFoundError,4003 PreviewWebContentsNotFoundError,
3932 PreviewWebviewNotInitializedError,4004 PreviewWebviewNotInitializedError,
4005 PreviewMainWindowClosedError,
3933 PreviewOperationError,4006 PreviewOperationError,
3934 PreviewArtifactPathOutsideDirectoryError,4007 PreviewArtifactPathOutsideDirectoryError,
3935 PreviewArtifactImageLoadError,4008 PreviewArtifactImageLoadError,
apps/desktop/src/window/DesktopWindow.test.ts+1-1
...@@ -455,7 +455,7 @@ describe("DesktopWindow", () => {...@@ -455,7 +455,7 @@ describe("DesktopWindow", () => {
455 assert.isUndefined(createdWindowOptions[0]?.x);455 assert.isUndefined(createdWindowOptions[0]?.x);
456 assert.isUndefined(createdWindowOptions[0]?.y);456 assert.isUndefined(createdWindowOptions[0]?.y);
457 assert.isTrue(createdWindowOptions[0]?.disableAutoHideCursor);457 assert.isTrue(createdWindowOptions[0]?.disableAutoHideCursor);
458 assert.isFalse(createdWindowOptions[0]?.webPreferences?.backgroundThrottling);458 assert.isUndefined(createdWindowOptions[0]?.webPreferences?.backgroundThrottling);
459 assert.deepEqual(fakeWindow.setAutoHideCursor.mock.calls, [[false]]);459 assert.deepEqual(fakeWindow.setAutoHideCursor.mock.calls, [[false]]);
460 assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["t3code-dev://app/"]);460 assert.deepEqual(fakeWindow.loadURL.mock.calls[0], ["t3code-dev://app/"]);
461 assert.equal(fakeWindow.openDevTools.mock.calls.length, 1);461 assert.equal(fakeWindow.openDevTools.mock.calls.length, 1);
apps/desktop/src/window/DesktopWindow.ts-1
...@@ -359,7 +359,6 @@ export const make = Effect.gen(function* () {...@@ -359,7 +359,6 @@ export const make = Effect.gen(function* () {
359 ...getWindowTitleBarOptions(shouldUseDarkColors, environment.platform),359 ...getWindowTitleBarOptions(shouldUseDarkColors, environment.platform),
360 webPreferences: {360 webPreferences: {
361 preload: environment.preloadPath,361 preload: environment.preloadPath,
362 backgroundThrottling: false,
363 contextIsolation: true,362 contextIsolation: true,
364 nodeIntegration: false,363 nodeIntegration: false,
365 sandbox: true,364 sandbox: true,