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

WebSocket ipc typing proposal #5125

Merged
merged 5 commits into from
Sep 2, 2022
Merged
Show file tree
Hide file tree
Changes from 4 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
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
84 changes: 47 additions & 37 deletions packages/insomnia/src/main/network/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,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 @@ -99,7 +99,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 @@ -118,10 +118,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 @@ -142,8 +142,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 @@ -322,9 +323,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 @@ -335,9 +336,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 @@ -347,19 +348,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'] {
return WebSocketConnections.get(options.requestId)?.readyState ?? 0;
}
): Promise<WebSocketConnection['readyState']> => {
return Promise.resolve(WebSocketConnections.get(options.requestId)?.readyState ?? 0);
jackkav marked this conversation as resolved.
Show resolved Hide resolved
};

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 @@ -394,38 +394,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 @@ -442,16 +440,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 @@ -39,7 +39,7 @@ const ActionButton: FC<ActionButtonProps> = ({ requestId, readyState }) => {
name="websocketActionCloseBtn"
type="button"
onClick={() => {
window.main.webSocketConnection.close({ requestId });
window.main.webSocket.close({ requestId });
}}
>
Close
Expand Down Expand Up @@ -79,7 +79,7 @@ const WebSocketIcon = styled.span({
export const WebSocketActionBar: FC<ActionBarProps> = ({ requestId, workspaceId, defaultValue, onChange, readyState }) => {
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