Skip to content

Commit

Permalink
SSE/EventStream support (Kong#6147)
Browse files Browse the repository at this point in the history
* new request

* add button

* s

* fix formatting issue

* detect event stream

* hide dropdown

* wip

* add basic test

* pass 1 can make connection

* implement event stream open pass 2

* wire up ready state

* wiring up close and error pass 3

* can open and close connections

* clean header

* listen to timeline

* extract options to function

* fix bug in ws

* add debug stuff to timeline

* don't rely on redux to set active response

* fix close type

* rename sse to event stream

* get request flag from request model

* copy websocket response pane

* hide response data

* flatten ws and curl responses

* fix test

* fix catch

* reset file

* use realtime event watcher

* rename some files

* fix types

* fix e2e test

* fix lint

* consistent empty states

* pin to bottom

* remove todo

* add SSE logo

* add sse to readme
  • Loading branch information
jackkav committed Nov 24, 2023
1 parent d4604c6 commit 858a5aa
Show file tree
Hide file tree
Showing 31 changed files with 854 additions and 298 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,6 @@
},
"files.insertFinalNewline": true,
"editor.formatOnSave": true,
"editor.formatOnSaveMode": "modificationsIfAvailable",
"editor.formatOnSaveMode": "file",
"editor.defaultFormatter": "vscode.typescript-language-features",
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![Slack Channel](https://chat.insomnia.rest/badge.svg)](https://chat.insomnia.rest/)
[![license](https://img.shields.io/github/license/Kong/insomnia.svg)](LICENSE)

Insomnia is an open-source, cross-platform API client for GraphQL, REST, WebSockets and gRPC.
Insomnia is an open-source, cross-platform API client for GraphQL, REST, WebSockets, Server-sent events and gRPC.

![Insomnia API Client](https://raw.githubusercontent.com/Kong/insomnia/develop/screenshots/main.png)

Expand Down
34 changes: 26 additions & 8 deletions packages/insomnia-smoke-test/fixtures/smoke-test-collection.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,31 @@ resources:
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: req_e6fab3568ed54f9d83c92e2c8006e150
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636141084436
created: 1636141078601
url: http://127.0.0.1:4010/events
name: connects to event stream and shows ping response
description: ""
method: GET
body: {}
parameters: []
headers:
- id: pair_4c3fe3092f1245eab6d960c633d8be8c
name: Accept
value: text/event-stream
description: ""
authentication: {}
metaSortKey: -1636141078601
isPrivate: false
settingStoreCookies: true
settingSendCookies: true
settingDisableRenderRequestBody: false
settingEncodeUrl: true
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: req_06a660bffe724e3fac14d7d09b4a5c9b
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636141067089
Expand Down Expand Up @@ -197,11 +222,4 @@ resources:
settingRebuildPath: true
settingFollowRedirects: global
_type: request
- _id: spc_255904e6a8774d24b29c9c3718feb07f
parentId: wrk_5b5ab67830944ffcbec47528366ef403
modified: 1636140994428
created: 1636140994428
fileName: Smoke tests
contents: ""
contentType: yaml
_type: api_spec

11 changes: 9 additions & 2 deletions packages/insomnia-smoke-test/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import crypto from 'node:crypto';

import express from 'express';
import { graphqlHTTP } from 'express-graphql';
import { readFileSync } from 'fs';
import { createServer } from 'https';
import crypto from 'node:crypto';
import { join } from 'path';

import { basicAuthRouter } from './basic-auth';
Expand Down Expand Up @@ -42,7 +43,7 @@ gitlabApi(app);

app.get('/delay/seconds/:duration', (req, res) => {
const delaySec = Number.parseInt(req.params.duration || '2');
setTimeout(function() {
setTimeout(function () {
res.send(`Delayed by ${delaySec} seconds`);
}, delaySec * 1000);
});
Expand Down Expand Up @@ -77,6 +78,12 @@ app.get('/events', (request, response) => {
response,
};
subscribers.push(subscriber);
setInterval(() => {
// const id = subscriberId;
const data = JSON.stringify({ message: 'Time: ' + new Date().toISOString().slice(11, 19) });
// response.write('id: ' + id + '\n');
response.write('data: ' + data + '\n\n');
}, 1000);
request.on('close', () => {
console.log(`${subscriberId} Connection closed`);
subscribers = subscribers.filter(sub => sub.id !== subscriberId);
Expand Down
21 changes: 14 additions & 7 deletions packages/insomnia-smoke-test/tests/smoke/app.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,40 +22,47 @@ test('can send requests', async ({ app, page }) => {
await page.getByText('CollectionSmoke testsjust now').click();

await page.getByRole('button', { name: 'send JSON request' }).click();
await page.click('text=http://127.0.0.1:4010/pets/1Send >> button');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect(statusTag).toContainText('200 OK');
await expect(responseBody).toContainText('"id": "1"');
await page.getByRole('button', { name: 'Preview' }).click();
await page.getByRole('menuitem', { name: 'Raw Data' }).click();
await expect(responseBody).toContainText('{"id":"1"}');

await page.getByRole('button', { name: 'connects to event stream and shows ping response' }).click();
await page.getByTestId('request-pane').getByRole('button', { name: 'Connect' }).click();
await expect(statusTag).toContainText('200 OK');
await page.getByRole('tab', { name: 'Timeline' }).click();
await expect(responseBody).toContainText('Connected to 127.0.0.1');
await page.getByTestId('request-pane').getByRole('button', { name: 'Disconnect' }).click();

await page.getByRole('button', { name: 'sends dummy.csv request and shows rich response' }).click();
await page.click('text=http://127.0.0.1:4010/file/dummy.csvSend >> button');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect(statusTag).toContainText('200 OK');
await page.getByRole('button', { name: 'Preview' }).click();
await page.getByRole('menuitem', { name: 'Raw Data' }).click();
await expect(responseBody).toContainText('a,b,c');

await page.getByRole('button', { name: 'sends dummy.xml request and shows raw response' }).click();
await page.click('text=http://127.0.0.1:4010/file/dummy.xmlSend >> button');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect(statusTag).toContainText('200 OK');
await expect(responseBody).toContainText('xml version="1.0"');
await expect(responseBody).toContainText('<LoginResult>');

await page.getByRole('button', { name: 'sends dummy.pdf request and shows rich response' }).click();
await page.click('text=http://127.0.0.1:4010/file/dummy.pdfSend >> button');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect(statusTag).toContainText('200 OK');
// TODO(filipe): re-add a check for the preview that is less flaky
await page.getByRole('tab', { name: 'Timeline' }).click();
await page.locator('pre').filter({ hasText: '< Content-Type: application/pdf' }).click();

await page.getByRole('button', { name: 'sends request with basic authentication' }).click();
await page.click('text=http://127.0.0.1:4010/auth/basicSend >> button');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect(statusTag).toContainText('200 OK');
await expect(responseBody).toContainText('basic auth received');

await page.getByRole('button', { name: 'sends request with cookie and get cookie in response' }).click();
await page.click('text=http://127.0.0.1:4010/cookiesSend >> button');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();
await expect(statusTag).toContainText('200 OK');
await page.getByRole('tab', { name: 'Timeline' }).click();
await expect(responseBody).toContainText('Set-Cookie: insomnia-test-cookie=value123');
Expand All @@ -76,7 +83,7 @@ test('can cancel requests', async ({ app, page }) => {
await page.getByText('CollectionSmoke testsjust now').click();

await page.getByRole('button', { name: 'delayed request' }).click();
await page.click('text=http://127.0.0.1:4010/delay/seconds/20Send >> button');
await page.getByTestId('request-pane').getByRole('button', { name: 'Send' }).click();

await page.getByRole('button', { name: 'Cancel Request' }).click();
await page.click('text=Request was cancelled');
Expand Down
1 change: 1 addition & 0 deletions packages/insomnia/src/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ export const CONTENT_TYPE_JSON = 'application/json';
export const CONTENT_TYPE_PLAINTEXT = 'text/plain';
export const CONTENT_TYPE_XML = 'application/xml';
export const CONTENT_TYPE_YAML = 'text/yaml';
export const CONTENT_TYPE_EVENT_STREAM = 'text/event-stream';
export const CONTENT_TYPE_EDN = 'application/edn';
export const CONTENT_TYPE_FORM_URLENCODED = 'application/x-www-form-urlencoded';
export const CONTENT_TYPE_FORM_DATA = 'multipart/form-data';
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/main.development.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { validateInsomniaConfig } from './common/validate-insomnia-config';
import { registerElectronHandlers } from './main/ipc/electron';
import { registergRPCHandlers } from './main/ipc/grpc';
import { registerMainHandlers } from './main/ipc/main';
import { registerCurlHandlers } from './main/network/curl';
import { registerWebSocketHandlers } from './main/network/websocket';
import { initializeSentry, sentryWatchAnalyticsEnabled } from './main/sentry';
import { checkIfRestartNeeded } from './main/squirrel-startup';
Expand Down Expand Up @@ -70,6 +71,7 @@ app.on('ready', async () => {
registerMainHandlers();
registergRPCHandlers();
registerWebSocketHandlers();
registerCurlHandlers();

/**
* There's no option that prevents Electron from fetching spellcheck dictionaries from Chromium's CDN and passing a non-resolving URL is the only known way to prevent it from fetching.
Expand Down
2 changes: 2 additions & 0 deletions packages/insomnia/src/main/ipc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { exportAllWorkspaces } from '../export';
import { insomniaFetch } from '../insomniaFetch';
import installPlugin from '../install-plugin';
import { axiosRequest } from '../network/axios-request';
import { CurlBridgeAPI } from '../network/curl';
import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise';
import { WebSocketBridgeAPI } from '../network/websocket';
import { gRPCBridgeAPI } from './grpc';
Expand All @@ -34,6 +35,7 @@ export interface MainBridgeAPI {
on: (channel: string, listener: (event: IpcRendererEvent, ...args: any[]) => void) => () => void;
webSocket: WebSocketBridgeAPI;
grpc: gRPCBridgeAPI;
curl: CurlBridgeAPI;
trackSegmentEvent: (options: { event: string; properties?: Record<string, unknown> }) => void;
trackPageView: (options: { name: string }) => void;
axiosRequest: typeof axiosRequest;
Expand Down

0 comments on commit 858a5aa

Please sign in to comment.