Skip to content

Commit

Permalink
WebSocket ipc typing proposal (#5125)
Browse files Browse the repository at this point in the history
* make consistent with main bridge

* rename webSocket

* remove deviated mock

* use consistent arrow function defintions

* Update packages/insomnia/src/main/network/websocket.ts

Co-authored-by: James Gatz <[email protected]>

Co-authored-by: James Gatz <[email protected]>
  • Loading branch information
jackkav and gatzjames authored Sep 2, 2022
1 parent e449c88 commit 0223b2e
Show file tree
Hide file tree
Showing 9 changed files with 74 additions and 154 deletions.
6 changes: 3 additions & 3 deletions packages/insomnia/src/main/ipc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { app, ipcMain, IpcRendererEvent } from 'electron';
import { writeFile } from 'fs/promises';

import { authorizeUserInWindow } from '../../network/o-auth-2/misc';
import { WSConnection } from '../../preload';
import installPlugin from '../install-plugin';
import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise';
import { WebSocketBridgeAPI } from '../network/websocket';

export interface MainBridgeAPI {
restart: () => void;
Expand All @@ -14,8 +14,8 @@ export interface MainBridgeAPI {
writeFile: (options: { path: string; content: string }) => Promise<string>;
cancelCurlRequest: typeof cancelCurlRequest;
curlRequest: typeof curlRequest;
on: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => Function;
webSocketConnection: WSConnection;
on: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => () => void;
webSocket: WebSocketBridgeAPI;
}
export function registerMainHandlers() {
ipcMain.handle('authorizeUserInWindow', (_, options: Parameters<typeof authorizeUserInWindow>[0]) => {
Expand Down
82 changes: 46 additions & 36 deletions packages/insomnia/src/main/network/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ const sendQueueMap = new Map<string, WebSocketEventLog>();
* When CTS is set, the events are sent immediately.
* If CTS is cleared, the events are batched into the send queue.
*/
function dispatchWebSocketEvent(target: Electron.WebContents, eventChannel: string, wsEvent: WebSocketEvent): void {
const dispatchWebSocketEvent = (target: Electron.WebContents, eventChannel: string, wsEvent: WebSocketEvent): void => {
// If the CTS flag is already set, just send immediately.
if (clearToSend) {
target.send(eventChannel, [wsEvent]);
Expand All @@ -98,7 +98,7 @@ function dispatchWebSocketEvent(target: Electron.WebContents, eventChannel: stri
} else {
sendQueueMap.set(eventChannel, [wsEvent]);
}
}
};

const parseResponseAndBuildTimeline = (url: string, incomingMessage: IncomingMessage, clientRequestHeaders: string) => {
const statusMessage = incomingMessage.statusMessage || '';
Expand All @@ -117,10 +117,10 @@ const parseResponseAndBuildTimeline = (url: string, incomingMessage: IncomingMes
return { timeline, responseHeaders, statusCode, statusMessage, httpVersion };
};

async function createWebSocketConnection(
const createWebSocketConnection = async (
event: Electron.IpcMainInvokeEvent,
options: { requestId: string; workspaceId: string }
) {
): Promise<void> => {
const existingConnection = WebSocketConnections.get(options.requestId);

if (existingConnection) {
Expand All @@ -141,8 +141,9 @@ async function createWebSocketConnection(
timelineFileStreams.set(options.requestId, fs.createWriteStream(timelinePath));

try {
const eventChannel = `webSocketRequest.connection.${responseId}.event`;
const readyStateChannel = `webSocketRequest.connection.${request._id}.readyState`;
const eventChannel = `webSocket.${responseId}.event`;
const readyStateChannel = `webSocket.${request._id}.readyState`;

// @TODO: Render nunjucks tags in these headers
const reduceArrayToLowerCaseKeyedDictionary = (acc: { [key: string]: string }, { name, value }: BaseWebSocketRequest['headers'][0]) =>
({ ...acc, [name.toLowerCase() || '']: value || '' });
Expand Down Expand Up @@ -321,9 +322,9 @@ async function createWebSocketConnection(
deleteRequestMaps(request._id, e.message || 'Something went wrong');
createErrorResponse(responseId, request._id, timelinePath, e.message || 'Something went wrong');
}
}
};

async function createErrorResponse(responseId: string, requestId: string, timelinePath: string, message: string) {
const createErrorResponse = async (responseId: string, requestId: string, timelinePath: string, message: string) => {
const settings = await models.settings.getOrCreate();
const responsePatch = {
_id: responseId,
Expand All @@ -334,9 +335,9 @@ async function createErrorResponse(responseId: string, requestId: string, timeli
};
models.response.create(responsePatch, settings.maxHistoryResponses);
models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: null });
}
};

async function deleteRequestMaps(requestId: string, message: string, event?: WebSocketCloseEvent | WebSocketErrorEvent,) {
const deleteRequestMaps = async (requestId: string, message: string, event?: WebSocketCloseEvent | WebSocketErrorEvent) => {
if (event) {
eventLogFileStreams.get(requestId)?.write(JSON.stringify(event) + '\n');
}
Expand All @@ -346,19 +347,18 @@ async function deleteRequestMaps(requestId: string, message: string, event?: Web
timelineFileStreams.get(requestId)?.end();
timelineFileStreams.delete(requestId);
WebSocketConnections.delete(requestId);
}
};

function getWebSocketReadyState(
_event: Electron.IpcMainInvokeEvent,
const getWebSocketReadyState = async (
options: { requestId: string }
): WebSocketConnection['readyState'] {
): Promise<WebSocketConnection['readyState']> => {
return WebSocketConnections.get(options.requestId)?.readyState ?? 0;
}
};

async function sendWebSocketEvent(
const sendWebSocketEvent = async (
event: Electron.IpcMainInvokeEvent,
options: { message: string; requestId: string }
) {
): Promise<void> => {
const ws = WebSocketConnections.get(options.requestId);

if (!ws) {
Expand Down Expand Up @@ -392,38 +392,36 @@ async function sendWebSocketEvent(
console.error('something went wrong');
return;
}
const eventChannel = `webSocketRequest.connection.${response._id}.event`;
const eventChannel = `webSocket.${response._id}.event`;
dispatchWebSocketEvent(event.sender, eventChannel, lastMessage);
}
};

async function closeWebSocketConnection(
_event: Electron.IpcMainInvokeEvent,
const closeWebSocketConnection = async (
options: { requestId: string }
) {
): Promise<void> => {
const ws = WebSocketConnections.get(options.requestId);
if (!ws) {
return;
}
ws.close();
}
};

async function findMany(
_event: Electron.IpcMainInvokeEvent,
const findMany = async (
options: { responseId: string }
) {
): Promise<WebSocketEvent[]> => {
const response = await models.response.getById(options.responseId);
if (!response || !response.bodyPath) {
return [];
}
const body = await fs.promises.readFile(response.bodyPath);
return body.toString().split('\n').filter(e => e?.trim())
.map(e => JSON.parse(e)) || [];
}
};

/**
* Sets the CTS flag; sent when the UI is ready for more events.
*/
function signalClearToSend(event: Electron.IpcMainInvokeEvent) {
const signalClearToSend = (event: Electron.IpcMainInvokeEvent): void => {
const nextChannel = sendQueueMap.keys().next();

// There are no pending events; just set the CTS flag.
Expand All @@ -440,16 +438,28 @@ function signalClearToSend(event: Electron.IpcMainInvokeEvent) {

event.sender.send(nextChannel.value, sendQueue);
sendQueueMap.delete(nextChannel.value);
}
};

export function registerWebSocketHandlers() {
ipcMain.handle('webSocketRequest.connection.create', createWebSocketConnection);
ipcMain.handle('webSocketRequest.connection.readyState', getWebSocketReadyState);
ipcMain.handle('webSocketRequest.connection.event.send', sendWebSocketEvent);
ipcMain.handle('webSocketRequest.connection.close', closeWebSocketConnection);
ipcMain.handle('webSocketRequest.connection.event.findMany', findMany);
ipcMain.handle('webSocketRequest.connection.clearToSend', signalClearToSend);
export interface WebSocketBridgeAPI {
create: (options: { requestId: string; workspaceId: string }) => void;
close: typeof closeWebSocketConnection;
readyState: {
getCurrent: typeof getWebSocketReadyState;
};
event: {
findMany: typeof findMany;
send: (options: { requestId: string; message: string }) => void;
clearToSend: () => void;
};
}
export const registerWebSocketHandlers = () => {
ipcMain.handle('webSocket.create', createWebSocketConnection);
ipcMain.handle('webSocket.event.send', sendWebSocketEvent);
ipcMain.handle('webSocket.clearToSend', signalClearToSend);
ipcMain.handle('webSocket.close', (_, options: Parameters<typeof closeWebSocketConnection>[0]) => closeWebSocketConnection(options));
ipcMain.handle('webSocket.readyState', (_, options: Parameters<typeof getWebSocketReadyState>[0]) => getWebSocketReadyState(options));
ipcMain.handle('webSocket.event.findMany', (_, options: Parameters<typeof findMany>[0]) => findMany(options));
};

electron.app.on('window-all-closed', () => {
WebSocketConnections.forEach(ws => {
Expand Down
79 changes: 11 additions & 68 deletions packages/insomnia/src/preload.ts
Original file line number Diff line number Diff line change
@@ -1,78 +1,21 @@
import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron';
import { contextBridge, ipcRenderer } from 'electron';

import type { WebSocketEvent } from './main/network/websocket';
import type { WebSocketBridgeAPI } from './main/network/websocket';

const webSocketConnection = {
create: (options: { requestId: string; workspaceId: string }) => {
return ipcRenderer.invoke('webSocketRequest.connection.create', options);
},
close: (options: { requestId: string }) => {
return ipcRenderer.invoke('webSocketRequest.connection.close', options);
},
const webSocket: WebSocketBridgeAPI = {
create: options => ipcRenderer.invoke('webSocket.create', options),
close: options => ipcRenderer.invoke('webSocket.close', options),
readyState: {
getCurrent: (options: { requestId: string }) => {
return ipcRenderer.invoke('webSocketRequest.connection.readyState', options);
},
subscribe: (
options: { requestId: string },
listener: (readyState: WebSocket['readyState']) => any
) => {
const channel = `webSocketRequest.connection.${options.requestId}.readyState`;

function onReadyStateChange(_event: IpcRendererEvent, readyState: WebSocket['readyState']) {
listener(readyState);
}

ipcRenderer.on(channel, onReadyStateChange);

const unsubscribe = () => {
ipcRenderer.off(channel, onReadyStateChange);
};

return unsubscribe;
},
getCurrent: options => ipcRenderer.invoke('webSocket.readyState', options),
},
event: {
findMany: (options: {
responseId: string;
}): Promise<WebSocketEvent[]> => {
return ipcRenderer.invoke(
'webSocketRequest.connection.event.findMany',
options
);
},
subscribe: (
options: { responseId: string },
listener: (webSocketEvents: WebSocketEvent[]) => any
) => {
const channel = `webSocketRequest.connection.${options.responseId}.event`;

function onNewEvent(_event: IpcRendererEvent, webSocketEvents: WebSocketEvent[]) {
listener(webSocketEvents);
}

ipcRenderer.on(channel, onNewEvent);

const unsubscribe = () => {
ipcRenderer.off(channel, onNewEvent);
};

return unsubscribe;
},
send(options: { requestId: string; message: string }) {
return ipcRenderer.invoke(
'webSocketRequest.connection.event.send',
options
);
},
clearToSend: () => {
return ipcRenderer.invoke('webSocketRequest.connection.clearToSend');
},
findMany: options => ipcRenderer.invoke('webSocket.event.findMany', options),
send: options => ipcRenderer.invoke('webSocket.event.send', options),
clearToSend: () => ipcRenderer.invoke('webSocket.clearToSend'),
},
};

export type WSConnection = typeof webSocketConnection; // using 'WS' because main/network/websocket.ts already has WebSocketConnection reserved.
const main: Window['main'] & { webSocketConnection: WSConnection } = {
const main: Window['main'] = {
restart: () => ipcRenderer.send('restart'),
authorizeUserInWindow: options => ipcRenderer.invoke('authorizeUserInWindow', options),
setMenuBarVisibility: options => ipcRenderer.send('setMenuBarVisibility', options),
Expand All @@ -84,7 +27,7 @@ const main: Window['main'] & { webSocketConnection: WSConnection } = {
ipcRenderer.on(channel, listener);
return () => ipcRenderer.removeListener(channel, listener);
},
webSocketConnection,
webSocket,
};
const dialog: Window['dialog'] = {
showOpenDialog: options => ipcRenderer.invoke('showOpenDialog', options),
Expand Down
4 changes: 2 additions & 2 deletions packages/insomnia/src/ui/components/websockets/action-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ const ActionButton: FC<ActionButtonProps> = ({ requestId, readyState }) => {
type="button"
warning
onClick={() => {
window.main.webSocketConnection.close({ requestId });
window.main.webSocket.close({ requestId });
}}
>
Disconnect
Expand Down Expand Up @@ -94,7 +94,7 @@ export const WebSocketActionBar: FC<ActionBarProps> = ({ requestId, workspaceId,
const isOpen = readyState === ReadyState.OPEN;
const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
window.main.webSocketConnection.create({ requestId, workspaceId });
window.main.webSocket.create({ requestId, workspaceId });
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const WebSocketRequestForm: FC<{ requestId: string }> = ({ requestId }) => {
const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const message = editorRef.current?.getValue() || '';
window.main.webSocketConnection.event.send({ requestId, message });
window.main.webSocket.event.send({ requestId, message });
};
return (
<SendMessageForm id="websocketMessageForm" onSubmit={handleSubmit}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ const WebSocketActiveResponsePane: FC<{ requestId: string; response: Response; h

const setActiveResponseAndDisconnect = (requestId: string, response: Response | null) => {
handleSetActiveResponse(requestId, response);
window.main.webSocketConnection.close({ requestId });
window.main.webSocket.close({ requestId });
};

useEffect(() => {
Expand Down
29 changes: 0 additions & 29 deletions packages/insomnia/src/ui/context/websocket-client/test-utils.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,28 +19,26 @@ export function useWebSocketConnectionEvents({ responseId }: { responseId: strin
// we don't lose any.
async function fetchAndSubscribeToEvents() {
// Fetch all existing events for this connection
const allEvents = await window.main.webSocketConnection.event.findMany({
responseId,
});
const allEvents = await window.main.webSocket.event.findMany({ responseId });
if (isMounted) {
setEvents(allEvents);
}
// Subscribe to new events and update the state.
unsubscribe = window.main.webSocketConnection.event.subscribe(
{ responseId },
events => {
unsubscribe = window.main.on(`webSocket.${responseId}.event`,
(_, events: WebSocketEvent[]) => {
console.log('received events', events);
if (isMounted) {
setEvents(allEvents => allEvents.concat(events));
}

// Wait to give the CTS signal until we've rendered a frame.
// This gives the UI a chance to render and respond to user interactions between receiving events.
// Note that we do this even if the component isn't mounted, to ensure that CTS gets set even if a race occurs.
window.requestAnimationFrame(window.main.webSocketConnection.event.clearToSend);
window.requestAnimationFrame(window.main.webSocket.event.clearToSend);
}
);

window.main.webSocketConnection.event.clearToSend();
window.main.webSocket.event.clearToSend();
}

fetchAndSubscribeToEvents();
Expand Down
Loading

0 comments on commit 0223b2e

Please sign in to comment.