Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Type safety for IPC messages #877

Merged
merged 9 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/__mocks__/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@ export function BrowserWindow() {}
BrowserWindow.prototype.loadURL = jest.fn();
BrowserWindow.prototype.isDestroyed = jest.fn( () => false );
BrowserWindow.prototype.on = jest.fn();
BrowserWindow.prototype.webContents = {

const mockWebContents = {
on: jest.fn(),
send: jest.fn(),
isDestroyed: jest.fn( () => false ),
};

BrowserWindow.prototype.webContents = mockWebContents;

BrowserWindow.fromWebContents = jest.fn( () => ( {
isDestroyed: jest.fn( () => false ),
webContents: {
isDestroyed: jest.fn( () => false ),
send: jest.fn(),
},
webContents: mockWebContents,
} ) );

BrowserWindow.getAllWindows = jest.fn( () => [] );
BrowserWindow.getFocusedWindow = jest.fn();

Expand Down
6 changes: 4 additions & 2 deletions src/components/auth-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,17 @@ const AuthProvider: React.FC< AuthProviderProps > = ( { children } ) => {

const authenticate = useCallback( () => getIpcApi().authenticate(), [] );

useIpcListener( 'auth-updated', ( _event, { token, error } ) => {
if ( error ) {
useIpcListener( 'auth-updated', ( _event, payload ) => {
if ( 'error' in payload ) {
getIpcApi().showErrorMessageBox( {
title: __( 'Authentication error' ),
message: __( 'Please try again.' ),
} );
return;
}

const { token } = payload;

setIsAuthenticated( true );
setClient( createWpcomClient( token.accessToken, locale ) );
setUser( {
Expand Down
34 changes: 14 additions & 20 deletions src/hooks/sync-sites/use-listen-deep-link-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,26 +13,20 @@ export function useListenDeepLinkConnection( {
const { selectedSite, setSelectedSiteId } = useSiteDetails();
const { setSelectedTab, selectedTab } = useContentTabs();

useIpcListener(
'sync-connect-site',
async (
_event,
{ remoteSiteId, studioSiteId }: { remoteSiteId: number; studioSiteId: string }
) => {
// Fetch latest sites from network before checking
const latestSites = await refetchSites();
const newConnectedSite = latestSites.find( ( site ) => site.id === remoteSiteId );
if ( newConnectedSite ) {
if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
// Select studio site that started the sync
setSelectedSiteId( studioSiteId );
}
await connectSite( newConnectedSite, studioSiteId );
if ( selectedTab !== 'sync' ) {
// Switch to sync tab
setSelectedTab( 'sync' );
}
useIpcListener( 'sync-connect-site', async ( _event, { remoteSiteId, studioSiteId } ) => {
// Fetch latest sites from network before checking
const latestSites = await refetchSites();
const newConnectedSite = latestSites.find( ( site ) => site.id === remoteSiteId );
if ( newConnectedSite ) {
if ( selectedSite?.id && selectedSite.id !== studioSiteId ) {
// Select studio site that started the sync
setSelectedSiteId( studioSiteId );
}
await connectSite( newConnectedSite, studioSiteId );
if ( selectedTab !== 'sync' ) {
// Switch to sync tab
setSelectedTab( 'sync' );
}
}
);
} );
}
2 changes: 1 addition & 1 deletion src/hooks/use-fullscreen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export function useFullscreen() {
};
}, [] );

useIpcListener( 'window-fullscreen-change', ( _, fullscreen: boolean ) => {
useIpcListener( 'window-fullscreen-change', ( _, fullscreen ) => {
setIsFullscreen( fullscreen );
} );

Expand Down
5 changes: 2 additions & 3 deletions src/hooks/use-import-export.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import { getIpcApi } from 'src/lib/get-ipc-api';
import { ExportEvents } from 'src/lib/import-export/export/events';
import { generateBackupFilename } from 'src/lib/import-export/export/generate-backup-filename';
import { BackupCreateProgressEventData, ExportOptions } from 'src/lib/import-export/export/types';
import { ImportExportEventData } from 'src/lib/import-export/handle-events';
import {
ImporterEvents,
BackupExtractEvents,
Expand Down Expand Up @@ -149,7 +148,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
[ importState ]
);

useIpcListener( 'on-import', ( _, { event, data }: ImportExportEventData, siteId: string ) => {
useIpcListener( 'on-import', ( _, { event, data }, siteId ) => {
if ( ! siteId ) {
return;
}
Expand Down Expand Up @@ -355,7 +354,7 @@ export const ImportExportProvider = ( { children }: { children: React.ReactNode
[ exportSite ]
);

useIpcListener( 'on-export', ( _, { event, data }: ImportExportEventData, siteId: string ) => {
useIpcListener( 'on-export', ( _, { event, data }, siteId ) => {
if ( ! siteId ) {
return;
}
Expand Down
7 changes: 6 additions & 1 deletion src/hooks/use-ipc-listener.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { IpcRendererEvent } from 'electron';
import { useEffect } from 'react';
import { IpcEvents } from 'src/ipc-utils';

export function useIpcListener( channel: string, listener: ( ...args: any[] ) => void ) {
export function useIpcListener< T extends keyof IpcEvents >(
channel: T,
listener: ( event: IpcRendererEvent, ...args: IpcEvents[ T ] ) => void
) {
useEffect( () => {
return window.ipcListener.subscribe( channel, listener );
}, [ channel, listener ] );
Expand Down
8 changes: 4 additions & 4 deletions src/hooks/use-theme-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
);
const [ loadingThumbnails, setLoadingThumbnails ] = useState< Record< string, boolean > >( {} );

useIpcListener( 'theme-details-changed', ( _evt, id, details ) => {
useIpcListener( 'theme-details-changed', ( _evt, { id, details } ) => {
setThemeDetails( ( themeDetails ) => {
return { ...themeDetails, [ id ]: details };
} );
Expand All @@ -54,16 +54,16 @@ export const ThemeDetailsProvider: React.FC< ThemeDetailsProviderProps > = ( { c
} );
} );

useIpcListener( 'thumbnail-changed', ( _evt, id, imageData ) => {
useIpcListener( 'thumbnail-changed', ( _evt, { id, imageData } ) => {
setThumbnails( ( thumbnails ) => {
return { ...thumbnails, [ id ]: imageData };
return { ...thumbnails, [ id ]: imageData ?? undefined };
} );
setLoadingThumbnails( ( loadingThumbnails ) => {
return { ...loadingThumbnails, [ id ]: false };
} );
} );

useIpcListener( 'theme-details-updating', ( _evt, id ) => {
useIpcListener( 'theme-details-updating', ( _evt, { id } ) => {
setLoadingThemeDetails( ( loadingThemeDetails ) => {
return { ...loadingThemeDetails, [ id ]: true };
} );
Expand Down
47 changes: 19 additions & 28 deletions src/ipc-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import * as Sentry from '@sentry/electron/main';
import { __, LocaleData, defaultI18n } from '@wordpress/i18n';
import archiver from 'archiver';
import { MAIN_MIN_WIDTH, SIDEBAR_WIDTH } from 'src/constants';
import { sendIpcEventToRendererWithWindow } from 'src/ipc-utils';
import { ACTIVE_SYNC_OPERATIONS } from 'src/lib/active-sync-operations';
import { calculateDirectorySize } from 'src/lib/calculate-directory-size';
import { download } from 'src/lib/download';
Expand Down Expand Up @@ -62,9 +63,10 @@ async function sendThumbnailChangedEvent( event: IpcMainInvokeEvent, id: string
}
const thumbnailData = await getThumbnailData( event, id );
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( parentWindow && ! parentWindow.isDestroyed() ) {
parentWindow.webContents.send( 'thumbnail-changed', id, thumbnailData );
}
sendIpcEventToRendererWithWindow( parentWindow, 'thumbnail-changed', {
id,
imageData: thumbnailData,
} );
}

async function mergeSiteDetailsWithRunningDetails(
Expand Down Expand Up @@ -112,9 +114,7 @@ export async function importSite(
try {
const onEvent = ( data: ImportExportEventData ) => {
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( parentWindow && ! parentWindow.isDestroyed() && ! event.sender.isDestroyed() ) {
parentWindow.webContents.send( 'on-import', data, id );
}
sendIpcEventToRendererWithWindow( parentWindow, 'on-import', data, id );
};
const result = await importBackup( backupFile, site.details, onEvent, defaultImporterOptions );
if ( result?.meta?.phpVersion ) {
Expand Down Expand Up @@ -189,9 +189,7 @@ export async function createSite(
}

const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( parentWindow && ! parentWindow.isDestroyed() && ! event.sender.isDestroyed() ) {
parentWindow.webContents.send( 'theme-details-updating', details.id );
}
sendIpcEventToRendererWithWindow( parentWindow, 'theme-details-updating', { id: details.id } );

userData.sites.push( server.details );
sortSites( userData.sites );
Expand Down Expand Up @@ -397,9 +395,11 @@ export async function startServer(
}
throw error;
}
if ( parentWindow && ! parentWindow.isDestroyed() && ! event.sender.isDestroyed() ) {
parentWindow.webContents.send( 'theme-details-changed', id, server.details.themeDetails );
}

sendIpcEventToRendererWithWindow( parentWindow, 'theme-details-changed', {
id,
details: server.details.themeDetails,
} );

if ( server.details.running ) {
try {
Expand Down Expand Up @@ -517,13 +517,7 @@ export async function getUserLocale( _event: IpcMainInvokeEvent ): Promise< Supp

export async function showUserSettings( event: IpcMainInvokeEvent ): Promise< void > {
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( ! parentWindow ) {
throw new Error( `No window found for sender of showUserSettings message: ${ event.frameId }` );
}
if ( parentWindow.isDestroyed() || event.sender.isDestroyed() ) {
return;
}
parentWindow.webContents.send( 'user-settings' );
sendIpcEventToRendererWithWindow( parentWindow, 'user-settings' );
}

function archiveWordPressDirectory( {
Expand Down Expand Up @@ -675,9 +669,7 @@ export async function exportSite(
try {
const onEvent = ( data: ImportExportEventData ) => {
const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( parentWindow && ! parentWindow.isDestroyed() && ! event.sender.isDestroyed() ) {
parentWindow.webContents.send( 'on-export', data, siteId );
}
sendIpcEventToRendererWithWindow( parentWindow, 'on-export', data, siteId );
};
return await exportBackup( options, onEvent );
} catch ( e ) {
Expand Down Expand Up @@ -805,16 +797,15 @@ export async function getThemeDetails( event: IpcMainInvokeEvent, id: string ) {

const parentWindow = BrowserWindow.fromWebContents( event.sender );
if ( themeDetails?.path && themeDetails.path !== server.details.themeDetails?.path ) {
if ( parentWindow && ! parentWindow.isDestroyed() && ! event.sender.isDestroyed() ) {
parentWindow.webContents.send( 'theme-details-updating', id );
}
sendIpcEventToRendererWithWindow( parentWindow, 'theme-details-updating', { id } );
const updatedSite = {
...server.details,
themeDetails,
};
if ( parentWindow && ! parentWindow.isDestroyed() && ! event.sender.isDestroyed() ) {
parentWindow.webContents.send( 'theme-details-changed', id, themeDetails );
}
sendIpcEventToRendererWithWindow( parentWindow, 'theme-details-changed', {
id,
details: themeDetails,
} );

server.updateCachedThumbnail().then( () => sendThumbnailChangedEvent( event, id ) );
server.details.themeDetails = themeDetails;
Expand Down
5 changes: 0 additions & 5 deletions src/ipc-types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,8 @@ interface AppGlobals {
quickDeploysEnabled: boolean;
}

interface IpcListener {
subscribe( channel: string, listener: ( ...args: any[] ) => void ): () => void;
}

// Our IPC objects will be attached to the `window` global
interface Window {
ipcListener: IpcListener;
ipcApi: IpcApi;
appGlobals: AppGlobals;
}
Expand Down
38 changes: 38 additions & 0 deletions src/ipc-utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { BrowserWindow } from 'electron';
import { ImportExportEventData } from 'src/lib/import-export/handle-events';
import { StoredToken } from 'src/lib/oauth';
import { getMainWindow } from 'src/main-window';

export interface IpcEvents {
'add-site': [ void ];
'auth-updated': [ { token: StoredToken } | { error: unknown } ];
'on-export': [ ImportExportEventData, string ];
'on-import': [ ImportExportEventData, string ];
'sync-connect-site': [ { remoteSiteId: number; studioSiteId: string } ];
'test-render-failure': [ void ];
'theme-details-changed': [ { id: string; details: StartedSiteDetails[ 'themeDetails' ] } ];
'theme-details-updating': [ { id: string } ];
'thumbnail-changed': [ { id: string; imageData: string | null } ];
'user-settings': [ void ];
'window-fullscreen-change': [ boolean ];
}

export async function sendIpcEventToRenderer< T extends keyof IpcEvents >(
channel: T,
...args: IpcEvents[ T ]
): Promise< void > {
const window = await getMainWindow();
if ( ! window.isDestroyed() && ! window.webContents.isDestroyed() ) {
window.webContents.send( channel, ...args );
}
}

export function sendIpcEventToRendererWithWindow< T extends keyof IpcEvents >(
window: BrowserWindow | null,
channel: T,
...args: IpcEvents[ T ]
): void {
if ( window && ! window.isDestroyed() && ! window.webContents.isDestroyed() ) {
window.webContents.send( channel, ...args );
}
}
11 changes: 4 additions & 7 deletions src/lib/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import * as Sentry from '@sentry/electron/main';
import wpcom from 'wpcom';
import { z } from 'zod';
import { PROTOCOL_PREFIX, WP_AUTHORIZE_ENDPOINT, CLIENT_ID, SCOPES } from 'src/constants';
import { sendIpcEventToRenderer } from 'src/ipc-utils';
import { shellOpenExternalWrapper } from 'src/lib/shell-open-external-wrapper';
import { getMainWindow } from 'src/main-window';
import { loadUserData, saveUserData } from 'src/storage/user-data';

const REDIRECT_URI = `${ PROTOCOL_PREFIX }://auth`;
Expand Down Expand Up @@ -115,20 +115,17 @@ export async function onOpenUrlCallback( url: string ) {
if ( host === 'auth' ) {
try {
const authResult = await handleAuthCallback( hash );
const mainWindow = await getMainWindow();
await storeToken( authResult );
mainWindow.webContents.send( 'auth-updated', { token: authResult } );
sendIpcEventToRenderer( 'auth-updated', { token: authResult } );
} catch ( error ) {
Sentry.captureException( error );
const mainWindow = await getMainWindow();
mainWindow.webContents.send( 'auth-updated', { error } );
sendIpcEventToRenderer( 'auth-updated', { error } );
}
} else if ( host === 'sync-connect-site' ) {
const remoteSiteId = parseInt( searchParams.get( 'remoteSiteId' ) ?? '' );
const studioSiteId = searchParams.get( 'studioSiteId' );
if ( remoteSiteId && studioSiteId ) {
const mainWindow = await getMainWindow();
mainWindow.webContents.send( 'sync-connect-site', { remoteSiteId, studioSiteId } );
sendIpcEventToRenderer( 'sync-connect-site', { remoteSiteId, studioSiteId } );
}
}
}
Loading
Loading