Skip to content

Commit bb5bf40

Browse files
authored
WebSocket response model (#5147)
* init websocket-response model * add websocket response model * remove unused timeline getter * remove unused functionality from the ws-response model
1 parent a8b1a42 commit bb5bf40

File tree

12 files changed

+239
-66
lines changed

12 files changed

+239
-66
lines changed

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

+12-16
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import { webSocketRequest } from '../../models';
2020
import * as models from '../../models';
2121
import { Environment } from '../../models/environment';
2222
import { RequestAuthentication, RequestHeader } from '../../models/request';
23-
import type { Response } from '../../models/response';
2423
import { BaseWebSocketRequest } from '../../models/websocket-request';
24+
import type { WebSocketResponse } from '../../models/websocket-response';
2525
import { getBasicAuthHeader } from '../../network/basic-auth/get-header';
2626
import { getBearerAuthHeader } from '../../network/bearer-auth/get-header';
2727
import { urlMatchesCertHost } from '../../network/url-matches-cert-host';
@@ -220,7 +220,7 @@ const createWebSocketConnection = async (
220220
const internalRequestHeader = ws._req._header;
221221
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
222222
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
223-
const responsePatch: Partial<Response> = {
223+
const responsePatch: Partial<WebSocketResponse> = {
224224
_id: responseId,
225225
parentId: request._id,
226226
environmentId: responseEnvironmentId,
@@ -231,20 +231,18 @@ const createWebSocketConnection = async (
231231
httpVersion,
232232
elapsedTime: performance.now() - start,
233233
timelinePath,
234-
bodyPath: responseBodyPath,
235-
// NOTE: required for legacy zip workaround
236-
bodyCompression: null,
234+
eventLogPath: responseBodyPath,
237235
};
238236
const settings = await models.settings.getOrCreate();
239-
models.response.create(responsePatch, settings.maxHistoryResponses);
237+
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
240238
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
241239
});
242240
ws.on('unexpected-response', async (clientRequest, incomingMessage) => {
243241
// @ts-expect-error -- private property
244242
const internalRequestHeader = clientRequest._header;
245243
const { timeline, responseHeaders, statusCode, statusMessage, httpVersion } = parseResponseAndBuildTimeline(options.url, incomingMessage, internalRequestHeader);
246244
timeline.map(t => timelineFileStreams.get(options.requestId)?.write(JSON.stringify(t) + '\n'));
247-
const responsePatch: Partial<Response> = {
245+
const responsePatch: Partial<WebSocketResponse> = {
248246
_id: responseId,
249247
parentId: request._id,
250248
environmentId: responseEnvironmentId,
@@ -255,12 +253,10 @@ const createWebSocketConnection = async (
255253
httpVersion,
256254
elapsedTime: performance.now() - start,
257255
timelinePath,
258-
bodyPath: responseBodyPath,
259-
// NOTE: required for legacy zip workaround
260-
bodyCompression: null,
256+
eventLogPath: responseBodyPath,
261257
};
262258
const settings = await models.settings.getOrCreate();
263-
models.response.create(responsePatch, settings.maxHistoryResponses);
259+
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
264260
models.requestMeta.updateOrCreateByParentId(request._id, { activeResponseId: null });
265261
deleteRequestMaps(request._id, `Unexpected response ${incomingMessage.statusCode}`);
266262
});
@@ -349,7 +345,7 @@ const createErrorResponse = async (responseId: string, requestId: string, enviro
349345
statusMessage: 'Error',
350346
error: message,
351347
};
352-
models.response.create(responsePatch, settings.maxHistoryResponses);
348+
models.webSocketResponse.create(responsePatch, settings.maxHistoryResponses);
353349
models.requestMeta.updateOrCreateByParentId(requestId, { activeResponseId: null });
354350
};
355351

@@ -402,7 +398,7 @@ const sendWebSocketEvent = async (
402398
};
403399

404400
eventLogFileStreams.get(options.requestId)?.write(JSON.stringify(lastMessage) + '\n');
405-
const response = await models.response.getLatestByParentId(options.requestId);
401+
const response = await models.webSocketResponse.getLatestByParentId(options.requestId);
406402
if (!response) {
407403
console.error('something went wrong');
408404
return;
@@ -428,11 +424,11 @@ const closeAllWebSocketConnections = (): void => {
428424
const findMany = async (
429425
options: { responseId: string }
430426
): Promise<WebSocketEvent[]> => {
431-
const response = await models.response.getById(options.responseId);
432-
if (!response || !response.bodyPath) {
427+
const response = await models.webSocketResponse.getById(options.responseId);
428+
if (!response || !response.eventLogPath) {
433429
return [];
434430
}
435-
const body = await fs.promises.readFile(response.bodyPath);
431+
const body = await fs.promises.readFile(response.eventLogPath);
436432
return body.toString().split('\n').filter(e => e?.trim())
437433
// Parse the message
438434
.map(e => JSON.parse(e))

packages/insomnia/src/models/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import * as _unitTest from './unit-test';
3737
import * as _unitTestResult from './unit-test-result';
3838
import * as _unitTestSuite from './unit-test-suite';
3939
import * as _webSocketRequest from './websocket-request';
40+
import * as _webSocketResponse from './websocket-response';
4041
import * as _workspace from './workspace';
4142
import * as _workspaceMeta from './workspace-meta';
4243

@@ -78,6 +79,7 @@ export const protoDirectory = _protoDirectory;
7879
export const grpcRequest = _grpcRequest;
7980
export const grpcRequestMeta = _grpcRequestMeta;
8081
export const webSocketRequest = _webSocketRequest;
82+
export const webSocketResponse = _webSocketResponse;
8183
export const workspace = _workspace;
8284
export const workspaceMeta = _workspaceMeta;
8385

@@ -112,6 +114,7 @@ export function all() {
112114
grpcRequest,
113115
grpcRequestMeta,
114116
webSocketRequest,
117+
webSocketResponse,
115118
] as const;
116119
}
117120

packages/insomnia/src/models/response.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -62,19 +62,19 @@ export function init(): BaseResponse {
6262
contentType: '',
6363
url: '',
6464
bytesRead: 0,
65-
bytesContent: -1,
6665
// -1 means that it was legacy and this property didn't exist yet
66+
bytesContent: -1,
6767
elapsedTime: 0,
6868
headers: [],
69-
timelinePath: '',
7069
// Actual timelines are stored on the filesystem
71-
bodyPath: '',
70+
timelinePath: '',
7271
// Actual bodies are stored on the filesystem
73-
bodyCompression: '__NEEDS_MIGRATION__',
72+
bodyPath: '',
7473
// For legacy bodies
74+
bodyCompression: '__NEEDS_MIGRATION__',
7575
error: '',
76-
requestVersionId: null,
7776
// Things from the request
77+
requestVersionId: null,
7878
settingStoreCookies: null,
7979
settingSendCookies: null,
8080
// Responses sent before environment filtering will have a special value
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import fs from 'fs';
2+
3+
import { database as db } from '../common/database';
4+
import * as requestOperations from './helpers/request-operations';
5+
import type { BaseModel } from './index';
6+
import * as models from './index';
7+
import { ResponseHeader } from './response';
8+
9+
export const name = 'WebSocket Response';
10+
11+
export const type = 'WebSocketResponse';
12+
13+
export const prefix = 'ws-res';
14+
15+
export const canDuplicate = false;
16+
17+
export const canSync = false;
18+
19+
export interface BaseWebSocketResponse {
20+
environmentId: string | null;
21+
statusCode: number;
22+
statusMessage: string;
23+
httpVersion: string;
24+
contentType: string;
25+
url: string;
26+
elapsedTime: number;
27+
headers: ResponseHeader[];
28+
// Event logs are stored on the filesystem
29+
eventLogPath: string;
30+
// Actual timelines are stored on the filesystem
31+
timelinePath: string;
32+
error: string;
33+
requestVersionId: string | null;
34+
settingStoreCookies: boolean | null;
35+
settingSendCookies: boolean | null;
36+
}
37+
38+
export type WebSocketResponse = BaseModel & BaseWebSocketResponse;
39+
40+
export const isWebSocketResponse = (model: Pick<BaseModel, 'type'>): model is WebSocketResponse => (
41+
model.type === type
42+
);
43+
44+
export function init(): BaseWebSocketResponse {
45+
return {
46+
statusCode: 0,
47+
statusMessage: '',
48+
httpVersion: '',
49+
contentType: '',
50+
url: '',
51+
elapsedTime: 0,
52+
headers: [],
53+
timelinePath: '',
54+
eventLogPath: '',
55+
error: '',
56+
requestVersionId: null,
57+
settingStoreCookies: null,
58+
settingSendCookies: null,
59+
environmentId: null,
60+
};
61+
}
62+
63+
export async function migrate(doc: Response) {
64+
return doc;
65+
}
66+
67+
export function hookDatabaseInit(consoleLog: typeof console.log = console.log) {
68+
consoleLog('[db] Init websocket-responses DB');
69+
}
70+
71+
export function hookRemove(doc: WebSocketResponse, consoleLog: typeof console.log = console.log) {
72+
fs.unlink(doc.eventLogPath, () => {
73+
consoleLog(`[response] Delete body ${doc.eventLogPath}`);
74+
});
75+
76+
fs.unlink(doc.timelinePath, () => {
77+
consoleLog(`[response] Delete timeline ${doc.timelinePath}`);
78+
});
79+
}
80+
81+
export function getById(id: string) {
82+
return db.get<WebSocketResponse>(type, id);
83+
}
84+
85+
export async function all() {
86+
return db.all<WebSocketResponse>(type);
87+
}
88+
89+
export async function removeForRequest(parentId: string, environmentId?: string | null) {
90+
const settings = await models.settings.getOrCreate();
91+
const query: Record<string, any> = {
92+
parentId,
93+
};
94+
95+
// Only add if not undefined. null is not the same as undefined
96+
// null: find responses sent from base environment
97+
// undefined: find all responses
98+
if (environmentId !== undefined && settings.filterResponsesByEnv) {
99+
query.environmentId = environmentId;
100+
}
101+
102+
// Also delete legacy responses here or else the user will be confused as to
103+
// why some responses are still showing in the UI.
104+
await db.removeWhere(type, query);
105+
}
106+
107+
export function remove(response: WebSocketResponse) {
108+
return db.remove(response);
109+
}
110+
111+
export async function create(patch: Partial<WebSocketResponse> = {}, maxResponses = 20) {
112+
if (!patch.parentId) {
113+
throw new Error('New Response missing `parentId`');
114+
}
115+
116+
const { parentId } = patch;
117+
// Create request version snapshot
118+
const request = await requestOperations.getById(parentId);
119+
const requestVersion = request ? await models.requestVersion.create(request) : null;
120+
patch.requestVersionId = requestVersion ? requestVersion._id : null;
121+
// Filter responses by environment if setting is enabled
122+
const query: Record<string, any> = {
123+
parentId,
124+
};
125+
126+
if (
127+
(await models.settings.getOrCreate()).filterResponsesByEnv &&
128+
patch.hasOwnProperty('environmentId')
129+
) {
130+
query.environmentId = patch.environmentId;
131+
}
132+
133+
// Delete all other responses before creating the new one
134+
const allResponses = await db.findMostRecentlyModified<WebSocketResponse>(type, query, Math.max(1, maxResponses));
135+
const recentIds = allResponses.map(r => r._id);
136+
// Remove all that were in the last query, except the first `maxResponses` IDs
137+
await db.removeWhere(type, {
138+
...query,
139+
_id: {
140+
$nin: recentIds,
141+
},
142+
});
143+
// Actually create the new response
144+
return db.docCreate(type, patch);
145+
}
146+
147+
export function getLatestByParentId(parentId: string) {
148+
return db.getMostRecentlyModified<WebSocketResponse>(type, {
149+
parentId,
150+
});
151+
}

packages/insomnia/src/ui/components/dropdowns/preview-mode-dropdown.tsx

+3-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { getPreviewModeName, PREVIEW_MODES, PreviewMode } from '../../../common/
66
import { exportHarCurrentRequest } from '../../../common/har';
77
import * as models from '../../../models';
88
import { isRequest } from '../../../models/request';
9+
import { isResponse } from '../../../models/response';
910
import { selectActiveRequest, selectActiveResponse, selectResponsePreviewMode } from '../../redux/selectors';
1011
import { Dropdown } from '../base/dropdown/dropdown';
1112
import { DropdownButton } from '../base/dropdown/dropdown-button';
@@ -36,7 +37,7 @@ export const PreviewModeDropdown: FC<Props> = ({
3637
const handleDownloadNormal = useCallback(() => download(false), [download]);
3738

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

6364
const exportDebugFile = useCallback(async () => {
64-
if (!response || !request) {
65+
if (!response || !request || !isResponse(response)) {
6566
console.warn('Nothing to download');
6667
return;
6768
}

0 commit comments

Comments
 (0)