SSE

Co-authored-by: Liyas Thomas <liyasthomas@gmail.com>
This commit is contained in:
Liyas Thomas
2019-11-25 05:20:41 +05:30
committed by GitHub
10 changed files with 491 additions and 299 deletions

View File

@@ -91,6 +91,10 @@ _Customized themes are also synced with local session storage_
- Send and receive data
📡 **Server Sent Events**: Receive a stream of updates from a server over a HTTP connection without resorting to polling.
- Receive data
🌍 **GraphQL**: GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data.
- Set endpoint and get schemas

View File

@@ -9,7 +9,7 @@ import * as querystring from "querystring";
*/
function joinDataArguments(dataArguments) {
let data = "";
dataArguments.forEach(function (argument, i) {
dataArguments.forEach((argument, i) => {
if (i === 0) {
data += argument;
} else {
@@ -55,7 +55,7 @@ function parseCurlCommand(curlCommand) {
if (!Array.isArray(parsedArguments[headerFieldName])) {
parsedArguments[headerFieldName] = [parsedArguments[headerFieldName]];
}
parsedArguments[headerFieldName].forEach(function (header) {
parsedArguments[headerFieldName].forEach((header) => {
if (header.includes("Cookie")) {
// stupid javascript tricks: closure
cookieString = header;
@@ -95,7 +95,7 @@ function parseCurlCommand(curlCommand) {
if (!Array.isArray(parsedArguments.F)) {
parsedArguments.F = [parsedArguments.F];
}
parsedArguments.F.forEach(function (multipartArgument) {
parsedArguments.F.forEach((multipartArgument) => {
// input looks like key=value. value could be json or a file path prepended with an @
const [key, value] = multipartArgument.split("=", 2);
multipartUploads[key] = value;

View File

@@ -43,8 +43,9 @@ try {
runCommand("git", ["branch"])
.split("* ")[1]
.split(" ")[0] + (IS_DEV_MODE ? " - DEV MODE" : "");
if (["", "master"].includes(version.variant))
if (["", "master"].includes(version.variant)) {
delete version.variant;
}
// Write version data into a file
fs.writeFileSync(

View File

@@ -101,9 +101,9 @@
<logo alt style="height: 24px;"></logo>
</nuxt-link>
<nuxt-link
to="/websocket"
:class="linkActive('/websocket')"
v-tooltip.right="'WebSocket'"
to="/realtime"
:class="linkActive('/realtime')"
v-tooltip.right="'Realtime'"
>
<i class="material-icons">settings_input_hdmi</i>
</nuxt-link>
@@ -144,7 +144,7 @@
</ul>
</nav>
</div>
<div v-else-if="['/websocket'].includes($route.path)">
<div v-else-if="['/realtime'].includes($route.path)">
<nav class="secondary-nav">
<ul>
<li>

View File

@@ -43,7 +43,7 @@ export default {
},
{
name: 'keywords',
content: 'postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket'
content: 'postwoman, postwoman chrome, postwoman online, postwoman for mac, postwoman app, postwoman for windows, postwoman google chrome, postwoman chrome app, get postwoman, postwoman web, postwoman android, postwoman app for chrome, postwoman mobile app, postwoman web app, api, request, testing, tool, rest, websocket, sse, graphql'
},
{
name: 'X-UA-Compatible',

View File

@@ -321,7 +321,7 @@ export default {
this.responseBodyMaxLines = (this.responseBodyMaxLines == Infinity) ? 16 : Infinity;
},
downloadResponse() {
var dataToWrite = JSON.stringify(this.schemaString, null, 2);
var dataToWrite = JSON.stringify(this.schemaString, null, 2)
var file = new Blob([dataToWrite], { type: "application/json" });
var a = document.createElement("a"),
url = URL.createObjectURL(file);
@@ -331,7 +331,7 @@ export default {
" on " +
Date() +
".graphql"
).replace(".", "[dot]");
).replace(/\./g, "[dot]");
document.body.appendChild(a);
a.click();
this.$refs.downloadResponse.innerHTML = this.doneButton;

View File

@@ -88,21 +88,23 @@
<option value="Users"></option>
</datalist>
</li>
<li>
<label class="hide-on-small-screen" for="send">&nbsp;</label>
<button
:disabled="!isValidURL"
@click="sendRequest"
id="send"
ref="sendButton"
>
Send
<span id="hidden-message">Again</span>
<span>
<i class="material-icons">send</i>
</span>
</button>
</li>
<ul>
<li>
<label class="hide-on-small-screen" for="send">&nbsp;</label>
<button
:disabled="!isValidURL"
@click="sendRequest"
id="send"
ref="sendButton"
>
Send
<span id="hidden-message">Again</span>
<span>
<i class="material-icons">send</i>
</span>
</button>
</li>
</ul>
</ul>
<div
class="blue"
@@ -1735,7 +1737,7 @@ export default {
this.method +
"] on " +
Date()
).replace(".", "[dot]");
).replace(/\./g, "[dot]");
document.body.appendChild(a);
a.click();
this.$refs.downloadResponse.innerHTML = this.doneButton;

442
pages/realtime.vue Normal file
View File

@@ -0,0 +1,442 @@
<template>
<div class="page">
<section id="options">
<input id="tab-one" type="radio" name="options" checked="checked" />
<label for="tab-one">WebSocket</label>
<div class="tab">
<pw-section class="blue" label="Request" ref="request">
<ul>
<li>
<label for="url">URL</label>
<input
id="url"
type="url"
:class="{ error: !urlValid }"
v-model="url"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
</li>
<div>
<li>
<label for="connect" class="hide-on-small-screen">&nbsp;</label>
<button
:disabled="!urlValid"
id="connect"
name="connect"
@click="toggleConnection"
>
{{ toggleConnectionVerb }}
<span>
<i class="material-icons" v-if="!connectionState">sync</i>
<i class="material-icons" v-if="connectionState"
>sync_disabled</i
>
</span>
</button>
</li>
</div>
</ul>
</pw-section>
<pw-section
class="purple"
label="Communication"
id="response"
ref="response"
>
<ul>
<li>
<label for="log">Log</label>
<div id="log" name="log" class="log">
<span v-if="communication.log">
<span
v-for="(logEntry, index) in communication.log"
:style="{ color: logEntry.color }"
:key="index"
>@ {{ logEntry.ts }} {{ getSourcePrefix(logEntry.source) }} {{ logEntry.payload }}</span>
</span>
<span v-else>(waiting for connection)</span>
</div>
</li>
</ul>
<ul>
<li>
<label for="message">Message</label>
<input
id="message"
name="message"
type="text"
v-model="communication.input"
:readonly="!connectionState"
@keyup.enter="connectionState ? sendMessage() : null"
/>
</li>
<div>
<li>
<label for="send" class="hide-on-small-screen">&nbsp;</label>
<button
id="send"
name="send"
:disabled="!connectionState"
@click="sendMessage"
>
Send
<span>
<i class="material-icons">send</i>
</span>
</button>
</li>
</div>
</ul>
</pw-section>
</div>
<input id="tab-two" type="radio" name="options" />
<label for="tab-two">SSE</label>
<div class="tab">
<pw-section class="blue" label="Request" ref="request">
<ul>
<li>
<label for="server">Server</label>
<input
id="server"
type="url"
:class="{ error: !serverValid }"
v-model="server"
@keyup.enter="serverValid ? toggleSSEConnection() : null"
/>
</li>
<div>
<li>
<label for="start" class="hide-on-small-screen">&nbsp;</label>
<button
:disabled="!serverValid"
id="start"
name="start"
@click="toggleSSEConnection"
>
{{ toggleSSEConnectionVerb }}
<span>
<i class="material-icons" v-if="!connectionSSEState">sync</i>
<i class="material-icons" v-if="connectionSSEState"
>sync_disabled</i
>
</span>
</button>
</li>
</div>
</ul>
</pw-section>
<pw-section
class="purple"
label="Communication"
id="response"
ref="response"
>
<ul>
<li>
<label for="log">Events</label>
<div id="log" name="log" class="log">
<span v-if="events.log">
<span
v-for="(logEntry, index) in events.log"
:style="{ color: logEntry.color }"
:key="index"
>@ {{ logEntry.ts }} {{ getSourcePrefix(logEntry.source) }} {{ logEntry.payload }}</span>
</span>
<span v-else>(waiting for connection)</span>
</div>
<div id="result"></div>
</li>
</ul>
</pw-section>
</div>
</section>
</div>
</template>
<style lang="scss">
div.log {
margin: 4px;
padding: 8px 16px;
width: calc(100% - 8px);
border-radius: 8px;
background-color: var(--bg-dark-color);
color: var(--fg-color);
height: 256px;
overflow: auto;
&,
span {
font-size: 16px;
font-family: "Roboto Mono", monospace;
font-weight: 400;
}
span {
display: block;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
}
}
</style>
<script>
export default {
components: {
"pw-section": () => import("../components/section")
},
data() {
return {
connectionState: false,
url: "wss://echo.websocket.org",
socket: null,
communication: {
log: null,
input: ""
},
connectionSSEState: false,
server: "https://express-eventsource.herokuapp.com/events",
sse: null,
events: {
log: null,
input: ""
}
};
},
computed: {
toggleConnectionVerb() {
return !this.connectionState ? "Connect" : "Disconnect";
},
urlValid() {
const pattern = new RegExp(
"^(wss?:\\/\\/)?" +
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" +
"((\\d{1,3}\\.){3}\\d{1,3}))" +
"(\\:\\d+)?(\\/[-a-z\\d%_.~+@]*)*" +
"(\\?[;&a-z\\d%_.~+=-]*)?" +
"(\\#[-a-z\\d_]*)?$",
"i"
);
return pattern.test(this.url);
},
toggleSSEConnectionVerb() {
return !this.connectionSSEState ? "Start" : "Stop";
},
serverValid() {
const pattern = new RegExp(
"^(http(s)?:\\/\\/)?" +
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" +
"((\\d{1,3}\\.){3}\\d{1,3}))" +
"(\\:\\d+)?(\\/[-a-z\\d%_.~+@]*)*" +
"(\\?[:\\;&a-z\\d%_.~+=-]*)?" +
"(\\#[-a-z\\d_]*)?$",
"i"
);
return pattern.test(this.server);
}
},
methods: {
toggleConnection() {
// If it is connecting:
if (!this.connectionState) return this.connect();
// Otherwise, it's disconnecting.
else return this.disconnect();
},
connect() {
this.communication.log = [
{
payload: `Connecting to ${this.url}...`,
source: "info",
color: "var(--ac-color)"
}
];
try {
this.socket = new WebSocket(this.url);
this.socket.onopen = event => {
this.connectionState = true;
this.communication.log = [
{
payload: `Connected to ${this.url}.`,
source: "info",
color: "var(--ac-color)",
ts: new Date().toLocaleTimeString()
}
];
this.$toast.success("Connected", {
icon: "sync"
});
};
this.socket.onerror = event => {
this.handleError();
};
this.socket.onclose = event => {
this.connectionState = false;
this.communication.log.push({
payload: `Disconnected from ${this.url}.`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
this.$toast.error("Disconnected", {
icon: "sync_disabled"
});
};
this.socket.onmessage = event => {
this.communication.log.push({
payload: event.data,
source: "server",
ts: new Date().toLocaleTimeString()
});
};
} catch (ex) {
this.handleError(ex);
this.$toast.error("Something went wrong!", {
icon: "error"
});
}
},
disconnect() {
if (this.socket !== null) this.socket.close();
},
handleError(error) {
this.disconnect();
this.connectionState = false;
this.communication.log.push({
payload: `An error has occurred.`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
if (error !== null)
this.communication.log.push({
payload: error,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
},
sendMessage() {
const message = this.communication.input;
this.socket.send(message);
this.communication.log.push({
payload: message,
source: "client",
ts: new Date().toLocaleTimeString()
});
this.communication.input = "";
},
collapse({ target }) {
const el = target.parentNode.className;
document.getElementsByClassName(el)[0].classList.toggle("hidden");
},
getSourcePrefix(source) {
const sourceEmojis = {
// Source used for info messages.
info: "\t [INFO]:\t",
// Source used for client to server messages.
client: "\t👽 [SENT]:\t",
// Source used for server to client messages.
server: "\t📥 [RECEIVED]:\t"
};
if (Object.keys(sourceEmojis).includes(source))
return sourceEmojis[source];
return "";
},
toggleSSEConnection() {
// If it is connecting:
if (!this.connectionSSEState) return this.start();
// Otherwise, it's disconnecting.
else return this.stop();
},
start() {
this.events.log = [
{
payload: `Connecting to ${this.server}...`,
source: "info",
color: "var(--ac-color)"
}
];
if(typeof(EventSource) !== "undefined") {
try {
this.sse = new EventSource(this.server);
this.sse.onopen = event => {
this.connectionSSEState = true;
this.events.log = [
{
payload: `Connected to ${this.server}.`,
source: "info",
color: "var(--ac-color)",
ts: new Date().toLocaleTimeString()
}
];
this.$toast.success("Connected", {
icon: "sync"
});
};
this.sse.onerror = event => {
this.handleSSEError();
};
this.sse.onclose = event => {
this.connectionSSEState = false;
this.events.log.push({
payload: `Disconnected from ${this.server}.`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
this.$toast.error("Disconnected", {
icon: "sync_disabled"
});
};
this.sse.onmessage = event => {
this.events.log.push({
payload: event.data,
source: "server",
ts: new Date().toLocaleTimeString()
});
};
} catch (ex) {
this.handleSSEError(ex);
this.$toast.error("Something went wrong!", {
icon: "error"
});
}
} else {
this.events.log = [
{
payload: `This browser doesn't seems to have Server Sent Events support.`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
}
];
}
},
handleSSEError(error) {
this.stop();
this.connectionSSEState = false;
this.events.log.push({
payload: `An error has occurred.`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
if (error !== null)
this.events.log.push({
payload: error,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
},
stop() {
this.sse.close();
}
},
updated: function() {
this.$nextTick(function() {
var divLog = document.getElementById("log");
divLog.scrollBy(0, divLog.scrollHeight + 100);
});
}
};
</script>

View File

@@ -1,265 +0,0 @@
<template>
<div class="page">
<pw-section class="blue" label="Request" ref="request">
<ul>
<li>
<label for="url">URL</label>
<input
id="url"
type="url"
:class="{ error: !urlValid }"
v-model="url"
@keyup.enter="urlValid ? toggleConnection() : null"
/>
</li>
<div>
<li>
<label for="connect" class="hide-on-small-screen">&nbsp;</label>
<button
:disabled="!urlValid"
id="connect"
name="connect"
@click="toggleConnection"
>
{{ toggleConnectionVerb }}
<span>
<i class="material-icons" v-if="!connectionState">sync</i>
<i class="material-icons" v-if="connectionState"
>sync_disabled</i
>
</span>
</button>
</li>
</div>
</ul>
</pw-section>
<pw-section
class="purple"
label="Communication"
id="response"
ref="response"
>
<ul>
<li>
<label for="log">Log</label>
<div id="log" name="log" class="log">
<span v-if="communication.log">
<span
v-for="(logEntry, index) in communication.log"
:style="{ color: logEntry.color }"
:key="index"
>@ {{ logEntry.ts }} {{ getSourcePrefix(logEntry.source) }} {{ logEntry.payload }}</span>
</span>
<span v-else>(waiting for connection)</span>
</div>
</li>
</ul>
<ul>
<li>
<label for="message">Message</label>
<input
id="message"
name="message"
type="text"
v-model="communication.input"
:readonly="!connectionState"
@keyup.enter="connectionState ? sendMessage() : null"
/>
</li>
<div>
<li>
<label for="send" class="hide-on-small-screen">&nbsp;</label>
<button
id="send"
name="send"
:disabled="!connectionState"
@click="sendMessage"
>
Send
<span>
<i class="material-icons">send</i>
</span>
</button>
</li>
</div>
</ul>
</pw-section>
</div>
</template>
<style lang="scss">
div.log {
margin: 4px;
padding: 8px 16px;
width: calc(100% - 8px);
border-radius: 8px;
background-color: var(--bg-dark-color);
color: var(--fg-color);
height: 256px;
overflow: auto;
&,
span {
font-size: 16px;
font-family: "Roboto Mono", monospace;
font-weight: 400;
}
span {
display: block;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
}
}
</style>
<script>
export default {
components: {
"pw-section": () => import("../components/section")
},
data() {
return {
connectionState: false,
url: "wss://echo.websocket.org",
socket: null,
communication: {
log: null,
input: ""
}
};
},
computed: {
toggleConnectionVerb() {
return !this.connectionState ? "Connect" : "Disconnect";
},
urlValid() {
const pattern = new RegExp(
"^(wss?:\\/\\/)?" +
"((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|" +
"((\\d{1,3}\\.){3}\\d{1,3}))" +
"(\\:\\d+)?(\\/[-a-z\\d%_.~+@]*)*" +
"(\\?[;&a-z\\d%_.~+=-]*)?" +
"(\\#[-a-z\\d_]*)?$",
"i"
);
return pattern.test(this.url);
}
},
methods: {
toggleConnection() {
// If it is connecting:
if (!this.connectionState) return this.connect();
// Otherwise, it's disconnecting.
else return this.disconnect();
},
connect() {
this.communication.log = [
{
payload: `Connecting to ${this.url}...`,
source: "info",
color: "var(--ac-color)"
}
];
try {
this.socket = new WebSocket(this.url);
this.socket.onopen = event => {
this.connectionState = true;
this.communication.log = [
{
payload: `Connected to ${this.url}.`,
source: "info",
color: "var(--ac-color)",
ts: new Date().toLocaleTimeString()
}
];
this.$toast.success("Connected", {
icon: "sync"
});
};
this.socket.onerror = event => {
this.handleError();
};
this.socket.onclose = event => {
this.connectionState = false;
this.communication.log.push({
payload: `Disconnected from ${this.url}.`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
this.$toast.error("Disconnected", {
icon: "sync_disabled"
});
};
this.socket.onmessage = event => {
this.communication.log.push({
payload: event.data,
source: "server",
ts: new Date().toLocaleTimeString()
});
};
} catch (ex) {
this.handleError(ex);
this.$toast.error("Something went wrong!", {
icon: "error"
});
}
},
disconnect() {
if (this.socket != null) this.socket.close();
},
handleError(error) {
this.disconnect();
this.connectionState = false;
this.communication.log.push({
payload: `An error has occurred.`,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
if (error != null)
this.communication.log.push({
payload: error,
source: "info",
color: "#ff5555",
ts: new Date().toLocaleTimeString()
});
},
sendMessage() {
const message = this.communication.input;
this.socket.send(message);
this.communication.log.push({
payload: message,
source: "client",
ts: new Date().toLocaleTimeString()
});
this.communication.input = "";
},
collapse({ target }) {
const el = target.parentNode.className;
document.getElementsByClassName(el)[0].classList.toggle("hidden");
},
getSourcePrefix(source) {
const sourceEmojis = {
// Source used for info messages.
info: "\t [INFO]:\t",
// Source used for client to server messages.
client: "\t👽 [SENT]:\t",
// Source used for server to client messages.
server: "\t📥 [RECEIVED]:\t"
};
if (Object.keys(sourceEmojis).includes(source))
return sourceEmojis[source];
return "";
}
},
updated: function() {
this.$nextTick(function() {
var divLog = document.getElementById("log");
divLog.scrollBy(0, divLog.scrollHeight + 100);
});
}
};
</script>

View File

@@ -73,15 +73,18 @@ export const state = () => ({
export const mutations = {
applySetting(state, setting) {
if (setting == null || !(setting instanceof Array) || setting.length !== 2)
if (setting == null || !(setting instanceof Array) || setting.length !== 2) {
throw new Error("You must provide a setting (array in the form [key, value])");
}
const [key, value] = setting;
// Do not just remove this check.
// Add your settings key to the SETTINGS_KEYS array at the
// top of the file.
// This is to ensure that application settings remain documented.
if (!SETTINGS_KEYS.includes(key)) throw new Error("The settings structure does not include the key " + key);
if (!SETTINGS_KEYS.includes(key)) {
throw new Error("The settings structure does not include the key " + key);
}
state.settings[key] = value;
},
@@ -182,17 +185,21 @@ export const mutations = {
const changedPlace = changedCollection || changedFolder
// set new request
if (requestNewFolderIndex !== undefined)
if (requestNewFolderIndex !== undefined) {
Vue.set(state.collections[requestNewCollectionIndex].folders[requestNewFolderIndex].requests, requestOldIndex, requestNew)
else
}
else {
Vue.set(state.collections[requestNewCollectionIndex].requests, requestOldIndex, requestNew)
}
// remove old request
if (changedPlace) {
if (requestOldFolderIndex !== undefined)
if (requestOldFolderIndex !== undefined) {
state.collections[requestOldCollectionIndex].folders[requestOldFolderIndex].requests.splice(requestOldIndex, 1)
else
}
else {
state.collections[requestOldCollectionIndex].requests.splice(requestOldIndex, 1)
}
}
},
@@ -208,8 +215,9 @@ export const mutations = {
const specifiedFolder = folderIndex !== undefined
const specifiedRequest = requestIndex !== undefined
if (specifiedCollection && specifiedFolder && specifiedRequest)
if (specifiedCollection && specifiedFolder && specifiedRequest) {
Vue.set(state.collections[collectionIndex].folders[folderIndex].requests, requestIndex, request)
}
else if (specifiedCollection && specifiedFolder && !specifiedRequest) {
const requests = state.collections[collectionIndex].folders[folderIndex].requests
const lastRequestIndex = requests.length - 1;