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 response model #5147

Merged
merged 4 commits into from
Sep 8, 2022
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
28 changes: 12 additions & 16 deletions packages/insomnia/src/main/network/websocket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import { webSocketRequest } from '../../models';
import * as models from '../../models';
import { Environment } from '../../models/environment';
import { RequestAuthentication, RequestHeader } from '../../models/request';
import type { Response } from '../../models/response';
import { BaseWebSocketRequest } from '../../models/websocket-request';
import type { WebSocketResponse } from '../../models/websocket-response';
import { getBasicAuthHeader } from '../../network/basic-auth/get-header';
import { getBearerAuthHeader } from '../../network/bearer-auth/get-header';
import { urlMatchesCertHost } from '../../network/url-matches-cert-host';
Expand Down Expand Up @@ -220,7 +220,7 @@ const createWebSocketConnection = async (
const internalRequestHeader = ws._req._header;
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
const responsePatch: Partial<Response> = {
const responsePatch: Partial<WebSocketResponse> = {
_id: responseId,
parentId: request._id,
environmentId: responseEnvironmentId,
Expand All @@ -231,20 +231,18 @@ const createWebSocketConnection = async (
httpVersion,
elapsedTime: performance.now() - start,
timelinePath,
bodyPath: responseBodyPath,
// NOTE: required for legacy zip workaround
bodyCompression: null,
eventLogPath: responseBodyPath,
};
const settings = await models.settings.getOrCreate();
models.response.create(responsePatch, settings.maxHistoryResponses);
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
});
ws.on('unexpected-response', async (clientRequest, incomingMessage) => {
// @ts-expect-error -- private property
const internalRequestHeader = clientRequest._header;
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
const responsePatch: Partial<Response> = {
const responsePatch: Partial<WebSocketResponse> = {
_id: responseId,
parentId: request._id,
environmentId: responseEnvironmentId,
Expand All @@ -255,12 +253,10 @@ const createWebSocketConnection = async (
httpVersion,
elapsedTime: performance.now() - start,
timelinePath,
bodyPath: responseBodyPath,
// NOTE: required for legacy zip workaround
bodyCompression: null,
eventLogPath: responseBodyPath,
};
const settings = await models.settings.getOrCreate();
models.response.create(responsePatch, settings.maxHistoryResponses);
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
deleteRequestMaps(request._id, `Unexpected response ${incomingMessage.statusCode}`);
});
Expand Down Expand Up @@ -349,7 +345,7 @@ const createErrorResponse = async (responseId: string, requestId: string, enviro
statusMessage: 'Error',
error: message,
};
models.response.create(responsePatch, settings.maxHistoryResponses);
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: null });
};

Expand Down Expand Up @@ -402,7 +398,7 @@ const sendWebSocketEvent = async (
};

eventLogFileStreams.get(options.requestId)?.write(JSON.stringify(lastMessage) + '\n');
const response = await models.response.getLatestByParentId(options.requestId);
const response = await models.webSocketResponse.getLatestByParentId(options.requestId);
if (!response) {
console.error('something went wrong');
return;
Expand All @@ -428,11 +424,11 @@ const closeAllWebSocketConnections = (): void => {
const findMany = async (
options: { responseId: string }
): Promise<WebSocketEvent[]> => {
const response = await models.response.getById(options.responseId);
if (!response || !response.bodyPath) {
const response = await models.webSocketResponse.getById(options.responseId);
if (!response || !response.eventLogPath) {
return [];
}
const body = await fs.promises.readFile(response.bodyPath);
const body = await fs.promises.readFile(response.eventLogPath);
return body.toString().split('\n').filter(e => e?.trim())
// Parse the message
.map(e => JSON.parse(e))
Expand Down
3 changes: 3 additions & 0 deletions packages/insomnia/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import * as _unitTest from './unit-test';
import * as _unitTestResult from './unit-test-result';
import * as _unitTestSuite from './unit-test-suite';
import * as _webSocketRequest from './websocket-request';
import * as _webSocketResponse from './websocket-response';
import * as _workspace from './workspace';
import * as _workspaceMeta from './workspace-meta';

Expand Down Expand Up @@ -78,6 +79,7 @@ export const protoDirectory = _protoDirectory;
export const grpcRequest = _grpcRequest;
export const grpcRequestMeta = _grpcRequestMeta;
export const webSocketRequest = _webSocketRequest;
export const webSocketResponse = _webSocketResponse;
export const workspace = _workspace;
export const workspaceMeta = _workspaceMeta;

Expand Down Expand Up @@ -112,6 +114,7 @@ export function all() {
grpcRequest,
grpcRequestMeta,
webSocketRequest,
webSocketResponse,
] as const;
}

Expand Down
10 changes: 5 additions & 5 deletions packages/insomnia/src/models/response.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,19 +62,19 @@ export function init(): BaseResponse {
contentType: '',
url: '',
bytesRead: 0,
bytesContent: -1,
// -1 means that it was legacy and this property didn't exist yet
bytesContent: -1,
elapsedTime: 0,
headers: [],
timelinePath: '',
// Actual timelines are stored on the filesystem
bodyPath: '',
timelinePath: '',
// Actual bodies are stored on the filesystem
bodyCompression: '__NEEDS_MIGRATION__',
bodyPath: '',
// For legacy bodies
bodyCompression: '__NEEDS_MIGRATION__',
error: '',
requestVersionId: null,
// Things from the request
requestVersionId: null,
settingStoreCookies: null,
settingSendCookies: null,
// Responses sent before environment filtering will have a special value
Expand Down
151 changes: 151 additions & 0 deletions packages/insomnia/src/models/websocket-response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import fs from 'fs';

import { database as db } from '../common/database';
import * as requestOperations from './helpers/request-operations';
import type { BaseModel } from './index';
import * as models from './index';
import { ResponseHeader } from './response';

export const name = 'WebSocket Response';

export const type = 'WebSocketResponse';

export const prefix = 'ws-res';

export const canDuplicate = false;

export const canSync = false;

export interface BaseWebSocketResponse {
environmentId: string | null;
statusCode: number;
statusMessage: string;
httpVersion: string;
contentType: string;
url: string;
elapsedTime: number;
headers: ResponseHeader[];
// Event logs are stored on the filesystem
eventLogPath: string;
// Actual timelines are stored on the filesystem
timelinePath: string;
error: string;
requestVersionId: string | null;
settingStoreCookies: boolean | null;
settingSendCookies: boolean | null;
}

export type WebSocketResponse = BaseModel & BaseWebSocketResponse;

export const isWebSocketResponse = (model: Pick<BaseModel, 'type'>): model is WebSocketResponse => (
model.type === type
);

export function init(): BaseWebSocketResponse {
return {
statusCode: 0,
statusMessage: '',
httpVersion: '',
contentType: '',
url: '',
elapsedTime: 0,
headers: [],
timelinePath: '',
eventLogPath: '',
error: '',
requestVersionId: null,
settingStoreCookies: null,
settingSendCookies: null,
environmentId: null,
};
}

export async function migrate(doc: Response) {
return doc;
}

export function hookDatabaseInit(consoleLog: typeof console.log = console.log) {
consoleLog('[db] Init websocket-responses DB');
}

export function hookRemove(doc: WebSocketResponse, consoleLog: typeof console.log = console.log) {
fs.unlink(doc.eventLogPath, () => {
consoleLog(`[response] Delete body ${doc.eventLogPath}`);
});

fs.unlink(doc.timelinePath, () => {
consoleLog(`[response] Delete timeline ${doc.timelinePath}`);
});
}

export function getById(id: string) {
return db.get<WebSocketResponse>(type, id);
}

export async function all() {
return db.all<WebSocketResponse>(type);
}

export async function removeForRequest(parentId: string, environmentId?: string | null) {
const settings = await models.settings.getOrCreate();
const query: Record<string, any> = {
parentId,
};

// Only add if not undefined. null is not the same as undefined
// null: find responses sent from base environment
// undefined: find all responses
if (environmentId !== undefined && settings.filterResponsesByEnv) {
query.environmentId = environmentId;
}

// Also delete legacy responses here or else the user will be confused as to
// why some responses are still showing in the UI.
await db.removeWhere(type, query);
}

export function remove(response: WebSocketResponse) {
return db.remove(response);
}

export async function create(patch: Partial<WebSocketResponse> = {}, maxResponses = 20) {
if (!patch.parentId) {
throw new Error('New Response missing `parentId`');
}

const { parentId } = patch;
// Create request version snapshot
const request = await requestOperations.getById(parentId);
const requestVersion = request ? await models.requestVersion.create(request) : null;
patch.requestVersionId = requestVersion ? requestVersion._id : null;
// Filter responses by environment if setting is enabled
const query: Record<string, any> = {
parentId,
};

if (
(await models.settings.getOrCreate()).filterResponsesByEnv &&
patch.hasOwnProperty('environmentId')
) {
query.environmentId = patch.environmentId;
}

// Delete all other responses before creating the new one
const allResponses = await db.findMostRecentlyModified<WebSocketResponse>(type, query, Math.max(1, maxResponses));
const recentIds = allResponses.map(r => r._id);
// Remove all that were in the last query, except the first `maxResponses` IDs
await db.removeWhere(type, {
...query,
_id: {
$nin: recentIds,
},
});
// Actually create the new response
return db.docCreate(type, patch);
}

export function getLatestByParentId(parentId: string) {
return db.getMostRecentlyModified<WebSocketResponse>(type, {
parentId,
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getPreviewModeName, PREVIEW_MODES, PreviewMode } from '../../../common/
import { exportHarCurrentRequest } from '../../../common/har';
import * as models from '../../../models';
import { isRequest } from '../../../models/request';
import { isResponse } from '../../../models/response';
import { selectActiveRequest, selectActiveResponse, selectResponsePreviewMode } from '../../redux/selectors';
import { Dropdown } from '../base/dropdown/dropdown';
import { DropdownButton } from '../base/dropdown/dropdown-button';
Expand Down Expand Up @@ -36,7 +37,7 @@ export const PreviewModeDropdown: FC<Props> = ({
const handleDownloadNormal = useCallback(() => download(false), [download]);

const exportAsHAR = useCallback(async () => {
if (!response || !request || !isRequest(request)) {
if (!response || !request || !isRequest(request) || !isResponse(response)) {
console.warn('Nothing to download');
return;
}
Expand All @@ -61,7 +62,7 @@ export const PreviewModeDropdown: FC<Props> = ({
}, [request, response]);

const exportDebugFile = useCallback(async () => {
if (!response || !request) {
if (!response || !request || !isResponse(response)) {
console.warn('Nothing to download');
return;
}
Expand Down
Loading