refactor: real-time system (#2228)
Co-authored-by: Andrew Bastin <andrewbastin.k@gmail.com> Co-authored-by: liyasthomas <liyascthomas@gmail.com>
This commit is contained in:
4
packages/hoppscotch-app/assets/icons/send.svg
Normal file
4
packages/hoppscotch-app/assets/icons/send.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="22" y1="2" x2="11" y2="13"></line>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 292 B |
@@ -28,7 +28,7 @@
|
||||
</Splitpanes>
|
||||
</Pane>
|
||||
<Pane
|
||||
v-if="SIDEBAR"
|
||||
v-if="SIDEBAR && hasSidebar"
|
||||
size="25"
|
||||
min-size="20"
|
||||
class="hide-scrollbar !overflow-auto flex flex-col"
|
||||
@@ -42,6 +42,7 @@
|
||||
import { Splitpanes, Pane } from "splitpanes"
|
||||
import "splitpanes/dist/splitpanes.css"
|
||||
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
|
||||
import { computed, useSlots } from "@nuxtjs/composition-api"
|
||||
import { useSetting } from "~/newstore/settings"
|
||||
|
||||
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
|
||||
@@ -52,4 +53,8 @@ const mdAndLarger = breakpoints.greater("md")
|
||||
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
|
||||
|
||||
const SIDEBAR = useSetting("SIDEBAR")
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const hasSidebar = computed(() => !!slots.sidebar)
|
||||
</script>
|
||||
|
||||
221
packages/hoppscotch-app/components/realtime/Communication.vue
Normal file
221
packages/hoppscotch-app/components/realtime/Communication.vue
Normal file
@@ -0,0 +1,221 @@
|
||||
<template>
|
||||
<div class="flex flex-col flex-1">
|
||||
<div v-if="showEventField" class="flex items-center justify-between p-4">
|
||||
<input
|
||||
id="event_name"
|
||||
v-model="eventName"
|
||||
class="input"
|
||||
name="event_name"
|
||||
:placeholder="`${t('socketio.event_name')}`"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperMobileSecondaryStickyFold sm:top-upperSecondaryStickyFold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("websocket.message") }}
|
||||
</label>
|
||||
<tippy
|
||||
ref="contentTypeOptions"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
:label="contentType || $t('state.none').toLowerCase()"
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col" role="menu">
|
||||
<SmartItem
|
||||
v-for="(contentTypeItem, index) in validContentTypes"
|
||||
:key="`contentTypeItem-${index}`"
|
||||
:label="contentTypeItem"
|
||||
:info-icon="contentTypeItem === contentType ? 'done' : ''"
|
||||
:active-info-icon="contentTypeItem === contentType"
|
||||
@click.native="
|
||||
() => {
|
||||
contentType = contentTypeItem
|
||||
$refs.contentTypeOptions.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip', delay: [500, 20], allowHTML: true }"
|
||||
:title="`${t('action.send')}`"
|
||||
:label="`${t('action.send')}`"
|
||||
:disabled="!communicationBody || !isConnected"
|
||||
svg="send"
|
||||
class="rounded-none !text-accent !hover:text-accentDark"
|
||||
@click.native="sendMessage()"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/body"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('state.linewrap')"
|
||||
:class="{ '!text-accent': linewrapEnabled }"
|
||||
svg="wrap-text"
|
||||
@click.native.prevent="linewrapEnabled = !linewrapEnabled"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
svg="trash-2"
|
||||
@click.native="clearContent"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="contentType && contentType == 'JSON'"
|
||||
ref="prettifyRequest"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.prettify')"
|
||||
:svg="prettifyIcon"
|
||||
@click.native="prettifyRequestBody"
|
||||
/>
|
||||
<label for="payload">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('import.title')"
|
||||
svg="file-plus"
|
||||
@click.native="$refs.payload.click()"
|
||||
/>
|
||||
</label>
|
||||
<input
|
||||
ref="payload"
|
||||
class="input"
|
||||
name="payload"
|
||||
type="file"
|
||||
@change="uploadPayload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div ref="wsCommunicationBody" class="flex flex-col flex-1"></div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from "@nuxtjs/composition-api"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as TO from "fp-ts/TaskOption"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { useCodemirror } from "~/helpers/editor/codemirror"
|
||||
import jsonLinter from "~/helpers/editor/linting/json"
|
||||
import { readFileAsText } from "~/helpers/functional/files"
|
||||
import { useI18n, useToast } from "~/helpers/utils/composables"
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
defineProps({
|
||||
showEventField: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isConnected: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(
|
||||
e: "send-message",
|
||||
body: {
|
||||
eventName: string
|
||||
message: string
|
||||
}
|
||||
): void
|
||||
}>()
|
||||
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
|
||||
const linewrapEnabled = ref(true)
|
||||
const wsCommunicationBody = ref<HTMLElement>()
|
||||
const prettifyIcon = ref<"wand" | "check" | "info">("wand")
|
||||
|
||||
const knownContentTypes = {
|
||||
JSON: "application/ld+json",
|
||||
Raw: "text/plain",
|
||||
} as const
|
||||
|
||||
const validContentTypes = Object.keys(knownContentTypes)
|
||||
|
||||
const contentType = ref<keyof typeof knownContentTypes>("JSON")
|
||||
const eventName = ref("")
|
||||
const communicationBody = ref("")
|
||||
|
||||
const rawInputEditorLang = computed(() => knownContentTypes[contentType.value])
|
||||
const langLinter = computed(() =>
|
||||
isJSONContentType(contentType.value) ? jsonLinter : null
|
||||
)
|
||||
|
||||
useCodemirror(
|
||||
wsCommunicationBody,
|
||||
communicationBody,
|
||||
reactive({
|
||||
extendedEditorConfig: {
|
||||
lineWrapping: linewrapEnabled,
|
||||
mode: rawInputEditorLang,
|
||||
placeholder: t("websocket.message").toString(),
|
||||
},
|
||||
linter: langLinter,
|
||||
completer: null,
|
||||
environmentHighlights: true,
|
||||
})
|
||||
)
|
||||
|
||||
const clearContent = () => {
|
||||
communicationBody.value = ""
|
||||
}
|
||||
|
||||
const sendMessage = () => {
|
||||
if (!communicationBody.value) return
|
||||
|
||||
emit("send-message", {
|
||||
eventName: eventName.value,
|
||||
message: communicationBody.value,
|
||||
})
|
||||
communicationBody.value = ""
|
||||
}
|
||||
|
||||
const uploadPayload = async (e: InputEvent) => {
|
||||
const result = await pipe(
|
||||
(e.target as HTMLInputElement).files?.[0],
|
||||
TO.fromNullable,
|
||||
TO.chain(readFileAsText)
|
||||
)()
|
||||
|
||||
if (O.isSome(result)) {
|
||||
communicationBody.value = result.value
|
||||
toast.success(`${t("state.file_imported")}`)
|
||||
} else {
|
||||
toast.error(`${t("action.choose_file")}`)
|
||||
}
|
||||
}
|
||||
const prettifyRequestBody = () => {
|
||||
try {
|
||||
const jsonObj = JSON.parse(communicationBody.value)
|
||||
communicationBody.value = JSON.stringify(jsonObj, null, 2)
|
||||
prettifyIcon.value = "check"
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
prettifyIcon.value = "info"
|
||||
toast.error(`${t("error.json_prettify_invalid_body")}`)
|
||||
}
|
||||
setTimeout(() => (prettifyIcon.value = "wand"), 1000)
|
||||
}
|
||||
</script>
|
||||
@@ -61,7 +61,8 @@ import { useThrottleFn, useScroll } from "@vueuse/core"
|
||||
import { useI18n } from "~/helpers/utils/composables"
|
||||
|
||||
export type LogEntryData = {
|
||||
ts: number
|
||||
prefix?: string
|
||||
ts: number | undefined
|
||||
source: "info" | "client" | "server" | "disconnected"
|
||||
payload: string
|
||||
event: "connecting" | "connected" | "disconnected" | "error"
|
||||
|
||||
@@ -11,7 +11,10 @@
|
||||
@click.native="copyQuery(entry.payload)"
|
||||
/>
|
||||
</div>
|
||||
<div class="items-center hidden px-1 w-18 sm:inline-flex">
|
||||
<div
|
||||
v-if="entry.ts !== undefined"
|
||||
class="items-center hidden px-1 w-18 sm:inline-flex"
|
||||
>
|
||||
<span
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="relativeTime"
|
||||
@@ -25,6 +28,9 @@
|
||||
@click="toggleExpandPayload()"
|
||||
>
|
||||
<div class="truncate">
|
||||
<span v-if="entry.prefix !== undefined" class="!inline">{{
|
||||
entry.prefix
|
||||
}}</span>
|
||||
{{ entry.payload }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -312,26 +318,20 @@ const copyQuery = (entry: string) => {
|
||||
}
|
||||
|
||||
// Relative Time
|
||||
const relativeTime = useTimeAgo(computed(() => props.entry.ts))
|
||||
// TS could be undefined here. We're just assigning a default value to 0 because we're not showing it in the UI
|
||||
const relativeTime = useTimeAgo(computed(() => props.entry.ts ?? 0))
|
||||
|
||||
const ENTRY_COLORS = {
|
||||
connected: "#10b981",
|
||||
connecting: "#10b981",
|
||||
error: "#ff5555",
|
||||
disconnected: "#ff5555",
|
||||
} as const
|
||||
|
||||
// Assigns color based on entry event
|
||||
const entryColor = computed(() => {
|
||||
switch (props.entry.event) {
|
||||
case "connected":
|
||||
return "#10b981"
|
||||
case "connecting":
|
||||
return "#10b981"
|
||||
case "error":
|
||||
return "#ff5555"
|
||||
case "disconnected":
|
||||
return "#ff5555"
|
||||
}
|
||||
})
|
||||
const entryColor = computed(() => ENTRY_COLORS[props.entry.event])
|
||||
|
||||
const ICONS: Record<
|
||||
LogEntryData["source"],
|
||||
{ iconName: string; iconColor: string }
|
||||
> = {
|
||||
const ICONS = {
|
||||
info: {
|
||||
iconName: "info-realtime",
|
||||
iconColor: "#10b981",
|
||||
@@ -348,7 +348,7 @@ const ICONS: Record<
|
||||
iconName: "info-disconnect",
|
||||
iconColor: "#ff5555",
|
||||
},
|
||||
}
|
||||
} as const
|
||||
|
||||
const iconColor = computed(() => ICONS[props.entry.source].iconColor)
|
||||
const iconName = computed(() => ICONS[props.entry.source].iconName)
|
||||
|
||||
@@ -13,17 +13,22 @@
|
||||
spellcheck="false"
|
||||
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
|
||||
:placeholder="$t('mqtt.url')"
|
||||
:disabled="connectionState"
|
||||
@keyup.enter="validUrl ? toggleConnection() : null"
|
||||
:disabled="
|
||||
connectionState === 'CONNECTED' ||
|
||||
connectionState === 'CONNECTING'
|
||||
"
|
||||
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="connect"
|
||||
:disabled="!validUrl"
|
||||
:disabled="!isUrlValid"
|
||||
class="w-32"
|
||||
:label="
|
||||
connectionState ? $t('action.disconnect') : $t('action.connect')
|
||||
connectionState === 'DISCONNECTED'
|
||||
? t('action.connect')
|
||||
: t('action.disconnect')
|
||||
"
|
||||
:loading="connectingState"
|
||||
:loading="connectionState === 'CONNECTING'"
|
||||
@click.native="toggleConnection"
|
||||
/>
|
||||
</div>
|
||||
@@ -56,14 +61,14 @@
|
||||
</template>
|
||||
<template #sidebar>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<label for="pub_topic" class="font-semibold text-secondaryLight">
|
||||
<label for="pubTopic" class="font-semibold text-secondaryLight">
|
||||
{{ $t("mqtt.topic") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex px-4">
|
||||
<input
|
||||
id="pub_topic"
|
||||
v-model="pub_topic"
|
||||
id="pubTopic"
|
||||
v-model="pubTopic"
|
||||
class="input"
|
||||
:placeholder="$t('mqtt.topic_name')"
|
||||
type="text"
|
||||
@@ -79,7 +84,7 @@
|
||||
<div class="flex px-4 space-x-2">
|
||||
<input
|
||||
id="mqtt-message"
|
||||
v-model="msg"
|
||||
v-model="message"
|
||||
class="input"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@@ -89,7 +94,7 @@
|
||||
<ButtonPrimary
|
||||
id="publish"
|
||||
name="get"
|
||||
:disabled="!canpublish"
|
||||
:disabled="!canPublish"
|
||||
:label="$t('mqtt.publish')"
|
||||
@click.native="publish"
|
||||
/>
|
||||
@@ -97,14 +102,14 @@
|
||||
<div
|
||||
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight"
|
||||
>
|
||||
<label for="sub_topic" class="font-semibold text-secondaryLight">
|
||||
<label for="subTopic" class="font-semibold text-secondaryLight">
|
||||
{{ $t("mqtt.topic") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex px-4 space-x-2">
|
||||
<input
|
||||
id="sub_topic"
|
||||
v-model="sub_topic"
|
||||
id="subTopic"
|
||||
v-model="subTopic"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:placeholder="$t('mqtt.topic_name')"
|
||||
@@ -114,7 +119,7 @@
|
||||
<ButtonPrimary
|
||||
id="subscribe"
|
||||
name="get"
|
||||
:disabled="!cansubscribe"
|
||||
:disabled="!canSubscribe"
|
||||
:label="
|
||||
subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
|
||||
"
|
||||
@@ -126,264 +131,220 @@
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
import Paho from "paho-mqtt"
|
||||
import debounce from "lodash/debounce"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
ref,
|
||||
watch,
|
||||
} from "@nuxtjs/composition-api"
|
||||
import debounce from "lodash/debounce"
|
||||
import { MQTTConnection, MQTTError } from "~/helpers/realtime/MQTTConnection"
|
||||
import {
|
||||
useI18n,
|
||||
useNuxt,
|
||||
useReadonlyStream,
|
||||
useStream,
|
||||
useStreamSubscriber,
|
||||
useToast,
|
||||
} from "~/helpers/utils/composables"
|
||||
import {
|
||||
MQTTEndpoint$,
|
||||
setMQTTEndpoint,
|
||||
MQTTConnectingState$,
|
||||
MQTTConnectionState$,
|
||||
setMQTTConnectingState,
|
||||
setMQTTConnectionState,
|
||||
MQTTSubscriptionState$,
|
||||
setMQTTSubscriptionState,
|
||||
MQTTSocket$,
|
||||
setMQTTSocket,
|
||||
MQTTLog$,
|
||||
setMQTTLog,
|
||||
addMQTTLogLine,
|
||||
MQTTConn$,
|
||||
MQTTEndpoint$,
|
||||
MQTTLog$,
|
||||
setMQTTConn,
|
||||
setMQTTEndpoint,
|
||||
setMQTTLog,
|
||||
} from "~/newstore/MQTTSession"
|
||||
import { useStream } from "~/helpers/utils/composables"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
url: useStream(MQTTEndpoint$, "", setMQTTEndpoint),
|
||||
connectionState: useStream(
|
||||
MQTTConnectionState$,
|
||||
false,
|
||||
setMQTTConnectionState
|
||||
),
|
||||
connectingState: useStream(
|
||||
MQTTConnectingState$,
|
||||
false,
|
||||
setMQTTConnectingState
|
||||
),
|
||||
subscriptionState: useStream(
|
||||
MQTTSubscriptionState$,
|
||||
false,
|
||||
setMQTTSubscriptionState
|
||||
),
|
||||
log: useStream(MQTTLog$, null, setMQTTLog),
|
||||
client: useStream(MQTTSocket$, null, setMQTTSocket),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isUrlValid: true,
|
||||
pub_topic: "",
|
||||
sub_topic: "",
|
||||
msg: "",
|
||||
manualDisconnect: false,
|
||||
username: "",
|
||||
password: "",
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
validUrl() {
|
||||
return this.isUrlValid
|
||||
},
|
||||
canpublish() {
|
||||
return this.pub_topic !== "" && this.msg !== "" && this.connectionState
|
||||
},
|
||||
cansubscribe() {
|
||||
return this.sub_topic !== "" && this.connectionState
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
url() {
|
||||
this.debouncer()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (process.browser) {
|
||||
this.worker = this.$worker.createRejexWorker()
|
||||
this.worker.addEventListener("message", this.workerResponseHandler)
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.worker.terminate()
|
||||
},
|
||||
methods: {
|
||||
debouncer: debounce(function () {
|
||||
this.worker.postMessage({ type: "ws", url: this.url })
|
||||
}, 1000),
|
||||
workerResponseHandler({ data }) {
|
||||
if (data.url === this.url) this.isUrlValid = data.result
|
||||
},
|
||||
connect() {
|
||||
this.connectingState = true
|
||||
this.log = [
|
||||
{
|
||||
payload: this.$t("state.connecting_to", { name: this.url }),
|
||||
source: "info",
|
||||
event: "connecting",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
const parseUrl = new URL(this.url)
|
||||
this.client = new Paho.Client(
|
||||
`${parseUrl.hostname}${
|
||||
parseUrl.pathname !== "/" ? parseUrl.pathname : ""
|
||||
}`,
|
||||
parseUrl.port !== "" ? Number(parseUrl.port) : 8081,
|
||||
"hoppscotch"
|
||||
)
|
||||
const connectOptions = {
|
||||
onSuccess: this.onConnectionSuccess,
|
||||
onFailure: this.onConnectionFailure,
|
||||
useSSL: parseUrl.protocol !== "ws:",
|
||||
}
|
||||
if (this.username !== "") {
|
||||
connectOptions.userName = this.username
|
||||
}
|
||||
if (this.password !== "") {
|
||||
connectOptions.password = this.password
|
||||
}
|
||||
this.client.connect(connectOptions)
|
||||
this.client.onConnectionLost = this.onConnectionLost
|
||||
this.client.onMessageArrived = this.onMessageArrived
|
||||
const t = useI18n()
|
||||
const nuxt = useNuxt()
|
||||
const toast = useToast()
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "mqtt",
|
||||
})
|
||||
},
|
||||
onConnectionFailure() {
|
||||
this.connectingState = false
|
||||
this.connectionState = false
|
||||
addMQTTLogLine({
|
||||
payload: this.$t("error.something_went_wrong"),
|
||||
source: "info",
|
||||
event: "error",
|
||||
ts: Date.now(),
|
||||
})
|
||||
},
|
||||
onConnectionSuccess() {
|
||||
this.connectingState = false
|
||||
this.connectionState = true
|
||||
addMQTTLogLine({
|
||||
payload: this.$t("state.connected_to", { name: this.url }),
|
||||
source: "info",
|
||||
event: "connected",
|
||||
ts: Date.now(),
|
||||
})
|
||||
this.$toast.success(this.$t("state.connected"))
|
||||
},
|
||||
onMessageArrived({ payloadString, destinationName }) {
|
||||
addMQTTLogLine({
|
||||
payload: `Message: ${payloadString} arrived on topic: ${destinationName}`,
|
||||
source: "info",
|
||||
event: "info",
|
||||
ts: Date.now(),
|
||||
})
|
||||
},
|
||||
toggleConnection() {
|
||||
if (this.connectionState) {
|
||||
this.disconnect()
|
||||
} else {
|
||||
this.connect()
|
||||
}
|
||||
},
|
||||
disconnect() {
|
||||
this.manualDisconnect = true
|
||||
this.client.disconnect()
|
||||
addMQTTLogLine({
|
||||
payload: this.$t("state.disconnected_from", { name: this.url }),
|
||||
source: "disconnected",
|
||||
event: "disconnected",
|
||||
ts: Date.now(),
|
||||
})
|
||||
},
|
||||
onConnectionLost() {
|
||||
this.connectingState = false
|
||||
this.connectionState = false
|
||||
if (this.manualDisconnect) {
|
||||
this.$toast.error(this.$t("state.disconnected"))
|
||||
} else {
|
||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||
}
|
||||
this.manualDisconnect = false
|
||||
this.subscriptionState = false
|
||||
},
|
||||
publish() {
|
||||
try {
|
||||
this.client.publish(this.pub_topic, this.msg, 0, false)
|
||||
const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
|
||||
const log = useStream(MQTTLog$, [], setMQTTLog)
|
||||
const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
|
||||
const connectionState = useReadonlyStream(
|
||||
socket.value.connectionState$,
|
||||
"DISCONNECTED"
|
||||
)
|
||||
const subscriptionState = useReadonlyStream(
|
||||
socket.value.subscriptionState$,
|
||||
false
|
||||
)
|
||||
|
||||
const isUrlValid = ref(true)
|
||||
const pubTopic = ref("")
|
||||
const subTopic = ref("")
|
||||
const message = ref("")
|
||||
const username = ref("")
|
||||
const password = ref("")
|
||||
|
||||
let worker: Worker
|
||||
|
||||
const canPublish = computed(
|
||||
() =>
|
||||
pubTopic.value !== "" &&
|
||||
message.value !== "" &&
|
||||
connectionState.value === "CONNECTED"
|
||||
)
|
||||
const canSubscribe = computed(
|
||||
() => subTopic.value !== "" && connectionState.value === "CONNECTED"
|
||||
)
|
||||
|
||||
const workerResponseHandler = ({
|
||||
data,
|
||||
}: {
|
||||
data: { url: string; result: boolean }
|
||||
}) => {
|
||||
if (data.url === url.value) isUrlValid.value = data.result
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
worker = nuxt.value.$worker.createRejexWorker()
|
||||
worker.addEventListener("message", workerResponseHandler)
|
||||
|
||||
subscribeToStream(socket.value.event$, (event) => {
|
||||
switch (event?.type) {
|
||||
case "CONNECTING":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: undefined,
|
||||
},
|
||||
]
|
||||
break
|
||||
|
||||
case "CONNECTED":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
toast.success(`${t("state.connected")}`)
|
||||
break
|
||||
|
||||
case "MESSAGE_SENT":
|
||||
addMQTTLogLine({
|
||||
payload: `Published message: ${this.msg} to topic: ${this.pub_topic}`,
|
||||
ts: Date.now(),
|
||||
source: "info",
|
||||
event: "info",
|
||||
})
|
||||
} catch (e) {
|
||||
addMQTTLogLine({
|
||||
payload:
|
||||
this.$t("error.something_went_wrong") +
|
||||
`while publishing msg: ${this.msg} to topic: ${this.pub_topic}`,
|
||||
source: "info",
|
||||
event: "error",
|
||||
prefix: `${event.message.topic}`,
|
||||
payload: event.message.message,
|
||||
source: "client",
|
||||
ts: Date.now(),
|
||||
})
|
||||
}
|
||||
},
|
||||
toggleSubscription() {
|
||||
if (this.subscriptionState) {
|
||||
this.unsubscribe()
|
||||
} else {
|
||||
this.subscribe()
|
||||
}
|
||||
},
|
||||
subscribe() {
|
||||
try {
|
||||
this.client.subscribe(this.sub_topic, {
|
||||
onSuccess: this.usubSuccess,
|
||||
onFailure: this.usubFailure,
|
||||
})
|
||||
} catch (e) {
|
||||
break
|
||||
|
||||
case "MESSAGE_RECEIVED":
|
||||
addMQTTLogLine({
|
||||
payload:
|
||||
this.$t("error.something_went_wrong") +
|
||||
`while subscribing to topic: ${this.sub_topic}`,
|
||||
source: "info",
|
||||
event: "error",
|
||||
ts: Date.now(),
|
||||
prefix: `${event.message.topic}`,
|
||||
payload: event.message.message,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
}
|
||||
},
|
||||
usubSuccess() {
|
||||
this.subscriptionState = !this.subscriptionState
|
||||
addMQTTLogLine({
|
||||
payload:
|
||||
`Successfully ` +
|
||||
(this.subscriptionState ? "subscribed" : "unsubscribed") +
|
||||
` to topic: ${this.sub_topic}`,
|
||||
source: "info",
|
||||
event: "info",
|
||||
ts: Date.now(),
|
||||
})
|
||||
},
|
||||
usubFailure() {
|
||||
addMQTTLogLine({
|
||||
payload:
|
||||
`Failed to ` +
|
||||
(this.subscriptionState ? "unsubscribe" : "subscribe") +
|
||||
` to topic: ${this.sub_topic}`,
|
||||
source: "info",
|
||||
color: "error",
|
||||
ts: Date.now(),
|
||||
})
|
||||
},
|
||||
unsubscribe() {
|
||||
this.client.unsubscribe(this.sub_topic, {
|
||||
onSuccess: this.usubSuccess,
|
||||
onFailure: this.usubFailure,
|
||||
})
|
||||
},
|
||||
clearLogEntries() {
|
||||
this.log = []
|
||||
},
|
||||
},
|
||||
break
|
||||
|
||||
case "SUBSCRIBED":
|
||||
addMQTTLogLine({
|
||||
payload: subscriptionState.value
|
||||
? `${t("state.subscribed_success", { topic: subTopic.value })}`
|
||||
: `${t("state.unsubscribed_success", { topic: subTopic.value })}`,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "SUBSCRIPTION_FAILED":
|
||||
addMQTTLogLine({
|
||||
payload: subscriptionState.value
|
||||
? `${t("state.subscribed_failed", { topic: subTopic.value })}`
|
||||
: `${t("state.unsubscribed_failed", { topic: subTopic.value })}`,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "ERROR":
|
||||
addMQTTLogLine({
|
||||
payload: getI18nError(event.error),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "DISCONNECTED":
|
||||
addMQTTLogLine({
|
||||
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
toast.error(`${t("state.disconnected")}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const debouncer = debounce(function () {
|
||||
worker.postMessage({ type: "ws", url: url.value })
|
||||
}, 1000)
|
||||
|
||||
watch(url, (newUrl) => {
|
||||
if (newUrl) debouncer()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
worker.terminate()
|
||||
})
|
||||
|
||||
// METHODS
|
||||
const toggleConnection = () => {
|
||||
// If it is connecting:
|
||||
if (connectionState.value === "DISCONNECTED") {
|
||||
return socket.value.connect(url.value, username.value, password.value)
|
||||
}
|
||||
// Otherwise, it's disconnecting.
|
||||
socket.value.disconnect()
|
||||
}
|
||||
const publish = () => {
|
||||
socket.value?.publish(pubTopic.value, message.value)
|
||||
}
|
||||
const toggleSubscription = () => {
|
||||
if (subscriptionState.value) {
|
||||
socket.value.unsubscribe(subTopic.value)
|
||||
} else {
|
||||
socket.value.subscribe(subTopic.value)
|
||||
}
|
||||
}
|
||||
|
||||
const getI18nError = (error: MQTTError): string => {
|
||||
if (typeof error === "string") return error
|
||||
|
||||
switch (error.type) {
|
||||
case "CONNECTION_NOT_ESTABLISHED":
|
||||
return t("state.connection_lost").toString()
|
||||
case "SUBSCRIPTION_FAILED":
|
||||
return t("state.mqtt_subscription_failed", {
|
||||
topic: error.topic,
|
||||
}).toString()
|
||||
case "PUBLISH_ERROR":
|
||||
return t("state.publish_error", { topic: error.topic }).toString()
|
||||
case "CONNECTION_LOST":
|
||||
return t("state.connection_lost").toString()
|
||||
case "CONNECTION_FAILED":
|
||||
return t("state.connection_failed").toString()
|
||||
default:
|
||||
return t("state.disconnected_from", { name: url.value }).toString()
|
||||
}
|
||||
}
|
||||
const clearLogEntries = () => {
|
||||
log.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -23,13 +23,16 @@
|
||||
class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26"
|
||||
:value="`Client ${clientVersion}`"
|
||||
readonly
|
||||
:disabled="connectionState"
|
||||
:disabled="
|
||||
connectionState === 'CONNECTED' ||
|
||||
connectionState === 'CONNECTING'
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col" role="menu">
|
||||
<SmartItem
|
||||
v-for="(_, version) in socketIoClients"
|
||||
v-for="version in SIOVersions"
|
||||
:key="`client-${version}`"
|
||||
:label="`Client ${version}`"
|
||||
@click.native="onSelectVersion(version)"
|
||||
@@ -43,487 +46,390 @@
|
||||
type="url"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:class="{ error: !urlValid }"
|
||||
:class="{ error: !isUrlValid }"
|
||||
class="flex flex-1 w-full px-4 py-2 border bg-primaryLight border-divider text-secondaryDark"
|
||||
:placeholder="$t('socketio.url')"
|
||||
:disabled="connectionState"
|
||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
||||
:placeholder="`${t('socketio.url')}`"
|
||||
:disabled="
|
||||
connectionState === 'CONNECTED' ||
|
||||
connectionState === 'CONNECTING'
|
||||
"
|
||||
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||
/>
|
||||
<input
|
||||
id="socketio-path"
|
||||
v-model="path"
|
||||
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
|
||||
spellcheck="false"
|
||||
:disabled="connectionState"
|
||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
||||
:disabled="
|
||||
connectionState === 'CONNECTED' ||
|
||||
connectionState === 'CONNECTING'
|
||||
"
|
||||
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||
/>
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
id="connect"
|
||||
:disabled="!urlValid"
|
||||
:disabled="!isUrlValid"
|
||||
name="connect"
|
||||
class="w-32"
|
||||
:label="
|
||||
!connectionState ? $t('action.connect') : $t('action.disconnect')
|
||||
connectionState === 'DISCONNECTED'
|
||||
? t('action.connect')
|
||||
: t('action.disconnect')
|
||||
"
|
||||
:loading="connectingState"
|
||||
:loading="connectionState === 'CONNECTING'"
|
||||
@click.native="toggleConnection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||
|
||||
<SmartTabs
|
||||
v-model="selectedTab"
|
||||
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("authorization.type") }}
|
||||
</label>
|
||||
<tippy
|
||||
ref="authTypeOptions"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
:label="authType"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col" role="menu">
|
||||
<SmartItem
|
||||
label="None"
|
||||
:icon="
|
||||
authType === 'None'
|
||||
? 'radio_button_checked'
|
||||
: 'radio_button_unchecked'
|
||||
"
|
||||
:active="authType === 'None'"
|
||||
@click.native="
|
||||
() => {
|
||||
authType = 'None'
|
||||
authTypeOptions.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="Bearer Token"
|
||||
:icon="
|
||||
authType === 'Bearer'
|
||||
? 'radio_button_checked'
|
||||
: 'radio_button_unchecked'
|
||||
"
|
||||
:active="authType === 'Bearer'"
|
||||
@click.native="
|
||||
() => {
|
||||
authType = 'Bearer'
|
||||
authTypeOptions.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<SmartCheckbox
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@change="authActive = !authActive"
|
||||
>
|
||||
{{ $t("state.enabled") }}
|
||||
</SmartCheckbox>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
:title="$t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.clear')"
|
||||
svg="trash-2"
|
||||
@click.native="clearContent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="authType === 'None'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/login.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="$t('empty.authorization')"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
This SocketIO connection does not use any authentication.
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
outline
|
||||
:label="$t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
svg="external-link"
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-if="authType === 'Bearer'"
|
||||
class="flex flex-1 border-b border-dividerLight"
|
||||
>
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
|
||||
<SmartTab
|
||||
:id="'communication'"
|
||||
:label="`${t('websocket.communication')}`"
|
||||
>
|
||||
<div class="p-2">
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ $t("helpers.authorization") }}
|
||||
<RealtimeCommunication
|
||||
:show-event-field="true"
|
||||
:is-connected="connectionState === 'CONNECTED'"
|
||||
@send-message="sendMessage($event)"
|
||||
></RealtimeCommunication>
|
||||
</SmartTab>
|
||||
<SmartTab :id="'protocols'" :label="`${t('request.authorization')}`">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||
>
|
||||
<span class="flex items-center">
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("authorization.type") }}
|
||||
</label>
|
||||
<tippy
|
||||
ref="authTypeOptions"
|
||||
interactive
|
||||
trigger="click"
|
||||
theme="popover"
|
||||
arrow
|
||||
>
|
||||
<template #trigger>
|
||||
<span class="select-wrapper">
|
||||
<ButtonSecondary
|
||||
class="pr-8 ml-2 rounded-none"
|
||||
:label="authType"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
<div class="flex flex-col" role="menu">
|
||||
<SmartItem
|
||||
label="None"
|
||||
:icon="
|
||||
authType === 'None'
|
||||
? 'radio_button_checked'
|
||||
: 'radio_button_unchecked'
|
||||
"
|
||||
:active="authType === 'None'"
|
||||
@click.native="
|
||||
() => {
|
||||
authType = 'None'
|
||||
authTypeOptions.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
<SmartItem
|
||||
label="Bearer Token"
|
||||
:icon="
|
||||
authType === 'Bearer'
|
||||
? 'radio_button_checked'
|
||||
: 'radio_button_unchecked'
|
||||
"
|
||||
:active="authType === 'Bearer'"
|
||||
@click.native="
|
||||
() => {
|
||||
authType = 'Bearer'
|
||||
authTypeOptions.tippy().hide()
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</tippy>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<SmartCheckbox
|
||||
:on="authActive"
|
||||
class="px-2"
|
||||
@change="authActive = !authActive"
|
||||
>
|
||||
{{ t("state.enabled") }}
|
||||
</SmartCheckbox>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
:title="t('app.wiki')"
|
||||
svg="help-circle"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear')"
|
||||
svg="trash-2"
|
||||
@click.native="clearContent"
|
||||
/>
|
||||
</div>
|
||||
<SmartAnchor
|
||||
class="link"
|
||||
:label="`${$t('authorization.learn')} \xA0 →`"
|
||||
</div>
|
||||
<div
|
||||
v-if="authType === 'None'"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/login.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="`${t('empty.authorization')}`"
|
||||
/>
|
||||
<span class="pb-4 text-center">
|
||||
{{ t("socketio.connection_not_authorized") }}
|
||||
</span>
|
||||
<ButtonSecondary
|
||||
outline
|
||||
:label="t('app.documentation')"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
svg="external-link"
|
||||
reverse
|
||||
class="mb-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="authType === 'Bearer'"
|
||||
class="flex flex-1 border-b border-dividerLight"
|
||||
>
|
||||
<div class="w-2/3 border-r border-dividerLight">
|
||||
<div class="flex flex-1 border-b border-dividerLight">
|
||||
<SmartEnvInput v-model="bearerToken" placeholder="Token" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky h-full p-4 overflow-auto bg-primary top-upperTertiaryStickyFold min-w-46 max-w-1/3 z-9"
|
||||
>
|
||||
<div class="p-2">
|
||||
<div class="pb-2 text-secondaryLight">
|
||||
{{ t("helpers.authorization") }}
|
||||
</div>
|
||||
<SmartAnchor
|
||||
class="link"
|
||||
:label="`${t('authorization.learn')} \xA0 →`"
|
||||
to="https://docs.hoppscotch.io/features/authorization"
|
||||
blank
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<RealtimeLog
|
||||
:title="$t('socketio.log')"
|
||||
:title="t('socketio.log')"
|
||||
:log="log"
|
||||
@delete="clearLogEntries()"
|
||||
/>
|
||||
</template>
|
||||
<template #sidebar>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<label for="events" class="font-semibold text-secondaryLight">
|
||||
{{ $t("socketio.events") }}
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex px-4">
|
||||
<input
|
||||
id="event_name"
|
||||
v-model="communication.eventName"
|
||||
class="input"
|
||||
name="event_name"
|
||||
:placeholder="$t('socketio.event_name')"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="!connectionState"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("socketio.communication") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('add.new')"
|
||||
svg="plus"
|
||||
@click.native="addCommunicationInput"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col px-4 pb-4 space-y-2">
|
||||
<div
|
||||
v-for="(input, index) of communication.inputs"
|
||||
:key="`input-${index}`"
|
||||
>
|
||||
<div class="flex space-x-2">
|
||||
<input
|
||||
v-model="communication.inputs[index]"
|
||||
class="input"
|
||||
name="message"
|
||||
:placeholder="$t('count.message', { count: index + 1 })"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="!connectionState"
|
||||
@keyup.enter="connectionState ? sendMessage() : null"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-if="index + 1 !== communication.inputs.length"
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
outline
|
||||
@click.native="removeCommunicationInput({ index })"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
v-if="index + 1 === communication.inputs.length"
|
||||
id="send"
|
||||
name="send"
|
||||
:disabled="!connectionState"
|
||||
:label="$t('action.send')"
|
||||
@click.native="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent, ref } from "@nuxtjs/composition-api"
|
||||
// All Socket.IO client version imports
|
||||
import ClientV2 from "socket.io-client-v2"
|
||||
import { io as ClientV3 } from "socket.io-client-v3"
|
||||
import { io as ClientV4 } from "socket.io-client-v4"
|
||||
import wildcard from "socketio-wildcard"
|
||||
<script setup lang="ts">
|
||||
import { onMounted, onUnmounted, ref, watch } from "@nuxtjs/composition-api"
|
||||
import debounce from "lodash/debounce"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import {
|
||||
SIOEndpoint$,
|
||||
setSIOEndpoint,
|
||||
SIOVersion$,
|
||||
setSIOVersion,
|
||||
SIOPath$,
|
||||
setSIOPath,
|
||||
SIOConnectionState$,
|
||||
SIOConnectingState$,
|
||||
setSIOConnectionState,
|
||||
setSIOConnectingState,
|
||||
SIOSocket$,
|
||||
setSIOSocket,
|
||||
SIOLog$,
|
||||
setSIOLog,
|
||||
SIOConnection,
|
||||
SIOError,
|
||||
SIOMessage,
|
||||
SOCKET_CLIENTS,
|
||||
} from "~/helpers/realtime/SIOConnection"
|
||||
import {
|
||||
useI18n,
|
||||
useNuxt,
|
||||
useReadonlyStream,
|
||||
useStream,
|
||||
useStreamSubscriber,
|
||||
useToast,
|
||||
} from "~/helpers/utils/composables"
|
||||
import {
|
||||
addSIOLogLine,
|
||||
setSIOEndpoint,
|
||||
setSIOLog,
|
||||
setSIOPath,
|
||||
setSIOVersion,
|
||||
SIOClientVersion,
|
||||
SIOEndpoint$,
|
||||
SIOLog$,
|
||||
SIOPath$,
|
||||
SIOVersion$,
|
||||
} from "~/newstore/SocketIOSession"
|
||||
import { useStream } from "~/helpers/utils/composables"
|
||||
|
||||
const socketIoClients = {
|
||||
v4: ClientV4,
|
||||
v3: ClientV3,
|
||||
v2: ClientV2,
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const nuxt = useNuxt()
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
type SIOTab = "communication" | "protocols"
|
||||
const selectedTab = ref<SIOTab>("communication")
|
||||
|
||||
const SIOVersions = Object.keys(SOCKET_CLIENTS)
|
||||
const url = useStream(SIOEndpoint$, "", setSIOEndpoint)
|
||||
const clientVersion = useStream(SIOVersion$, "v4", setSIOVersion)
|
||||
const path = useStream(SIOPath$, "", setSIOPath)
|
||||
const socket = new SIOConnection()
|
||||
const connectionState = useReadonlyStream(
|
||||
socket.connectionState$,
|
||||
"DISCONNECTED"
|
||||
)
|
||||
const log = useStream(SIOLog$, [], setSIOLog)
|
||||
const authTypeOptions = ref<any>(null)
|
||||
const versionOptions = ref<any | null>(null)
|
||||
|
||||
const isUrlValid = ref(true)
|
||||
const authType = ref<"None" | "Bearer">("None")
|
||||
const bearerToken = ref("")
|
||||
const authActive = ref(true)
|
||||
|
||||
let worker: Worker
|
||||
|
||||
const workerResponseHandler = ({
|
||||
data,
|
||||
}: {
|
||||
data: { url: string; result: boolean }
|
||||
}) => {
|
||||
if (data.url === url.value) isUrlValid.value = data.result
|
||||
}
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
socketIoClients,
|
||||
url: useStream(SIOEndpoint$, "", setSIOEndpoint),
|
||||
clientVersion: useStream(SIOVersion$, "", setSIOVersion),
|
||||
path: useStream(SIOPath$, "", setSIOPath),
|
||||
connectingState: useStream(
|
||||
SIOConnectingState$,
|
||||
false,
|
||||
setSIOConnectingState
|
||||
),
|
||||
connectionState: useStream(
|
||||
SIOConnectionState$,
|
||||
false,
|
||||
setSIOConnectionState
|
||||
),
|
||||
io: useStream(SIOSocket$, null, setSIOSocket),
|
||||
log: useStream(SIOLog$, [], setSIOLog),
|
||||
authTypeOptions: ref(null),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isUrlValid: true,
|
||||
communication: {
|
||||
eventName: "",
|
||||
inputs: [""],
|
||||
},
|
||||
authType: "None",
|
||||
bearerToken: "",
|
||||
authActive: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
urlValid() {
|
||||
return this.isUrlValid
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
url() {
|
||||
this.debouncer()
|
||||
},
|
||||
connectionState(connected) {
|
||||
if (connected) this.$refs.versionOptions.tippy().disable()
|
||||
else this.$refs.versionOptions.tippy().enable()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (process.browser) {
|
||||
this.worker = this.$worker.createRejexWorker()
|
||||
this.worker.addEventListener("message", this.workerResponseHandler)
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.worker.terminate()
|
||||
},
|
||||
methods: {
|
||||
debouncer: debounce(function () {
|
||||
this.worker.postMessage({ type: "socketio", url: this.url })
|
||||
}, 1000),
|
||||
workerResponseHandler({ data }) {
|
||||
if (data.url === this.url) this.isUrlValid = data.result
|
||||
},
|
||||
removeCommunicationInput({ index }) {
|
||||
this.$delete(this.communication.inputs, index)
|
||||
},
|
||||
addCommunicationInput() {
|
||||
this.communication.inputs.push("")
|
||||
},
|
||||
toggleConnection() {
|
||||
// If it is connecting:
|
||||
if (!this.connectionState) return this.connect()
|
||||
// Otherwise, it's disconnecting.
|
||||
else return this.disconnect()
|
||||
},
|
||||
connect() {
|
||||
this.connectingState = true
|
||||
this.log = [
|
||||
{
|
||||
payload: this.$t("state.connecting_to", { name: this.url }),
|
||||
source: "info",
|
||||
event: "connecting",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
const getMessagePayload = (data: SIOMessage): string =>
|
||||
typeof data.value === "object" ? JSON.stringify(data.value) : `${data.value}`
|
||||
|
||||
try {
|
||||
if (!this.path) {
|
||||
this.path = "/socket.io"
|
||||
}
|
||||
const Client = socketIoClients[this.clientVersion]
|
||||
if (this.authActive && this.authType === "Bearer") {
|
||||
this.io = new Client(this.url, {
|
||||
path: this.path,
|
||||
auth: {
|
||||
token: this.bearerToken,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.io = new Client(this.url, { path: this.path })
|
||||
}
|
||||
const getErrorPayload = (error: SIOError): string => {
|
||||
switch (error.type) {
|
||||
case "CONNECTION":
|
||||
return t("state.connection_error").toString()
|
||||
case "RECONNECT_ERROR":
|
||||
return t("state.reconnection_error").toString()
|
||||
default:
|
||||
return t("state.disconnected_from", { name: url.value }).toString()
|
||||
}
|
||||
}
|
||||
|
||||
// Add ability to listen to all events
|
||||
wildcard(Client.Manager)(this.io)
|
||||
this.io.on("connect", () => {
|
||||
this.connectingState = false
|
||||
this.connectionState = true
|
||||
this.log = [
|
||||
{
|
||||
payload: this.$t("state.connected_to", { name: this.url }),
|
||||
source: "info",
|
||||
event: "connected",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
this.$toast.success(this.$t("state.connected"))
|
||||
})
|
||||
this.io.on("*", ({ data }) => {
|
||||
const [eventName, message] = data
|
||||
addSIOLogLine({
|
||||
payload: `[${eventName}] ${message ? JSON.stringify(message) : ""}`,
|
||||
source: "server",
|
||||
ts: Date.now(),
|
||||
})
|
||||
})
|
||||
this.io.on("connect_error", (error) => {
|
||||
this.handleError(error)
|
||||
})
|
||||
this.io.on("reconnect_error", (error) => {
|
||||
this.handleError(error)
|
||||
})
|
||||
this.io.on("error", () => {
|
||||
this.handleError()
|
||||
})
|
||||
this.io.on("disconnect", () => {
|
||||
this.connectingState = false
|
||||
this.connectionState = false
|
||||
addSIOLogLine({
|
||||
payload: this.$t("state.disconnected_from", { name: this.url }),
|
||||
source: "disconnected",
|
||||
event: "disconnected",
|
||||
ts: Date.now(),
|
||||
})
|
||||
this.$toast.error(this.$t("state.disconnected"))
|
||||
})
|
||||
} catch (e) {
|
||||
this.handleError(e)
|
||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||
}
|
||||
onMounted(() => {
|
||||
worker = nuxt.value.$worker.createRejexWorker()
|
||||
worker.addEventListener("message", workerResponseHandler)
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "socketio",
|
||||
})
|
||||
},
|
||||
disconnect() {
|
||||
this.io.close()
|
||||
},
|
||||
handleError(error) {
|
||||
this.disconnect()
|
||||
this.connectingState = false
|
||||
this.connectionState = false
|
||||
addSIOLogLine({
|
||||
payload: this.$t("error.something_went_wrong"),
|
||||
source: "info",
|
||||
event: "error",
|
||||
ts: Date.now(),
|
||||
})
|
||||
if (error !== null)
|
||||
subscribeToStream(socket.event$, (event) => {
|
||||
switch (event?.type) {
|
||||
case "CONNECTING":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: undefined,
|
||||
},
|
||||
]
|
||||
break
|
||||
|
||||
case "CONNECTED":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: event.time,
|
||||
},
|
||||
]
|
||||
toast.success(`${t("state.connected")}`)
|
||||
break
|
||||
|
||||
case "MESSAGE_SENT":
|
||||
addSIOLogLine({
|
||||
payload: error,
|
||||
source: "info",
|
||||
event: "error",
|
||||
ts: Date.now(),
|
||||
})
|
||||
},
|
||||
sendMessage() {
|
||||
const eventName = this.communication.eventName
|
||||
const messages = (this.communication.inputs || [])
|
||||
.map((input) => {
|
||||
try {
|
||||
return JSON.parse(input)
|
||||
} catch (e) {
|
||||
return input
|
||||
}
|
||||
})
|
||||
.filter((message) => !!message)
|
||||
|
||||
if (this.io) {
|
||||
this.io.emit(eventName, ...messages, (data) => {
|
||||
// receive response from server
|
||||
addSIOLogLine({
|
||||
payload: `[${eventName}] ${JSON.stringify(data)}`,
|
||||
source: "server",
|
||||
ts: Date.now(),
|
||||
})
|
||||
})
|
||||
|
||||
addSIOLogLine({
|
||||
payload: `[${eventName}] ${JSON.stringify(messages)}`,
|
||||
prefix: `[${event.message.eventName}]`,
|
||||
payload: getMessagePayload(event.message),
|
||||
source: "client",
|
||||
ts: Date.now(),
|
||||
ts: event.time,
|
||||
})
|
||||
this.communication.inputs = [""]
|
||||
}
|
||||
},
|
||||
onSelectVersion(version) {
|
||||
this.clientVersion = version
|
||||
this.$refs.versionOptions.tippy().hide()
|
||||
},
|
||||
clearLogEntries() {
|
||||
this.log = []
|
||||
},
|
||||
},
|
||||
break
|
||||
|
||||
case "MESSAGE_RECEIVED":
|
||||
addSIOLogLine({
|
||||
prefix: `[${event.message.eventName}]`,
|
||||
payload: getMessagePayload(event.message),
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "ERROR":
|
||||
addSIOLogLine({
|
||||
payload: getErrorPayload(event.error),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "DISCONNECTED":
|
||||
addSIOLogLine({
|
||||
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
toast.error(`${t("state.disconnected")}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
watch(url, (newUrl) => {
|
||||
if (newUrl) debouncer()
|
||||
})
|
||||
|
||||
watch(connectionState, (connected) => {
|
||||
if (connected) versionOptions.value.tippy().disable()
|
||||
else versionOptions.value.tippy().enable()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
worker.terminate()
|
||||
})
|
||||
|
||||
const debouncer = debounce(function () {
|
||||
worker.postMessage({ type: "socketio", url: url.value })
|
||||
}, 1000)
|
||||
|
||||
const toggleConnection = () => {
|
||||
// If it is connecting:
|
||||
if (connectionState.value === "DISCONNECTED") {
|
||||
return socket.connect({
|
||||
url: url.value,
|
||||
path: path.value || "/socket.io",
|
||||
clientVersion: clientVersion.value,
|
||||
auth: authActive.value
|
||||
? {
|
||||
type: authType.value,
|
||||
token: bearerToken.value,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
}
|
||||
// Otherwise, it's disconnecting.
|
||||
socket.disconnect()
|
||||
}
|
||||
const sendMessage = (event: { message: string; eventName: string }) => {
|
||||
socket.sendMessage(event)
|
||||
}
|
||||
const onSelectVersion = (version: SIOClientVersion) => {
|
||||
clientVersion.value = version
|
||||
versionOptions.value.tippy().hide()
|
||||
}
|
||||
const clearLogEntries = () => {
|
||||
log.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -11,11 +11,13 @@
|
||||
v-model="server"
|
||||
type="url"
|
||||
autocomplete="off"
|
||||
:class="{ error: !serverValid }"
|
||||
:class="{ error: !isUrlValid }"
|
||||
class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark"
|
||||
:placeholder="$t('sse.url')"
|
||||
:disabled="connectionSSEState"
|
||||
@keyup.enter="serverValid ? toggleSSEConnection() : null"
|
||||
:disabled="
|
||||
connectionState === 'STARTED' || connectionState === 'STARTING'
|
||||
"
|
||||
@keyup.enter="isUrlValid ? toggleSSEConnection() : null"
|
||||
/>
|
||||
<label
|
||||
for="event-type"
|
||||
@@ -28,19 +30,23 @@
|
||||
v-model="eventType"
|
||||
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
|
||||
spellcheck="false"
|
||||
:disabled="connectionSSEState"
|
||||
@keyup.enter="serverValid ? toggleSSEConnection() : null"
|
||||
:disabled="
|
||||
connectionState === 'STARTED' || connectionState === 'STARTING'
|
||||
"
|
||||
@keyup.enter="isUrlValid ? toggleSSEConnection() : null"
|
||||
/>
|
||||
</div>
|
||||
<ButtonPrimary
|
||||
id="start"
|
||||
:disabled="!serverValid"
|
||||
:disabled="!isUrlValid"
|
||||
name="start"
|
||||
class="w-32"
|
||||
:label="
|
||||
!connectionSSEState ? $t('action.start') : $t('action.stop')
|
||||
connectionState === 'STOPPED'
|
||||
? t('action.start')
|
||||
: t('action.stop')
|
||||
"
|
||||
:loading="connectingState"
|
||||
:loading="connectionState === 'STARTING'"
|
||||
@click.native="toggleSSEConnection"
|
||||
/>
|
||||
</div>
|
||||
@@ -56,11 +62,10 @@
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted, onMounted } from "@nuxtjs/composition-api"
|
||||
import "splitpanes/dist/splitpanes.css"
|
||||
import debounce from "lodash/debounce"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import {
|
||||
SSEEndpoint$,
|
||||
setSSEEndpoint,
|
||||
@@ -68,163 +73,127 @@ import {
|
||||
setSSEEventType,
|
||||
SSESocket$,
|
||||
setSSESocket,
|
||||
SSEConnectingState$,
|
||||
SSEConnectionState$,
|
||||
setSSEConnectionState,
|
||||
setSSEConnectingState,
|
||||
SSELog$,
|
||||
setSSELog,
|
||||
addSSELogLine,
|
||||
} from "~/newstore/SSESession"
|
||||
import { useStream } from "~/helpers/utils/composables"
|
||||
import {
|
||||
useNuxt,
|
||||
useStream,
|
||||
useToast,
|
||||
useI18n,
|
||||
useStreamSubscriber,
|
||||
useReadonlyStream,
|
||||
} from "~/helpers/utils/composables"
|
||||
import { SSEConnection } from "~/helpers/realtime/SSEConnection"
|
||||
|
||||
export default defineComponent({
|
||||
setup() {
|
||||
return {
|
||||
connectionSSEState: useStream(
|
||||
SSEConnectionState$,
|
||||
false,
|
||||
setSSEConnectionState
|
||||
),
|
||||
connectingState: useStream(
|
||||
SSEConnectingState$,
|
||||
false,
|
||||
setSSEConnectingState
|
||||
),
|
||||
server: useStream(SSEEndpoint$, "", setSSEEndpoint),
|
||||
eventType: useStream(SSEEventType$, "", setSSEEventType),
|
||||
sse: useStream(SSESocket$, null, setSSESocket),
|
||||
log: useStream(SSELog$, [], setSSELog),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isUrlValid: true,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
serverValid() {
|
||||
return this.isUrlValid
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
server() {
|
||||
this.debouncer()
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (process.browser) {
|
||||
this.worker = this.$worker.createRejexWorker()
|
||||
this.worker.addEventListener("message", this.workerResponseHandler)
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.worker.terminate()
|
||||
},
|
||||
methods: {
|
||||
debouncer: debounce(function () {
|
||||
this.worker.postMessage({ type: "sse", url: this.server })
|
||||
}, 1000),
|
||||
workerResponseHandler({ data }) {
|
||||
if (data.url === this.url) this.isUrlValid = data.result
|
||||
},
|
||||
toggleSSEConnection() {
|
||||
// If it is connecting:
|
||||
if (!this.connectionSSEState) return this.start()
|
||||
// Otherwise, it's disconnecting.
|
||||
else return this.stop()
|
||||
},
|
||||
start() {
|
||||
this.connectingState = true
|
||||
this.log = [
|
||||
{
|
||||
payload: this.$t("state.connecting_to", { name: this.server }),
|
||||
source: "info",
|
||||
event: "connecting",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
if (typeof EventSource !== "undefined") {
|
||||
try {
|
||||
this.sse = new EventSource(this.server)
|
||||
this.sse.onopen = () => {
|
||||
this.connectingState = false
|
||||
this.connectionSSEState = true
|
||||
this.log = [
|
||||
{
|
||||
payload: this.$t("state.connected_to", { name: this.server }),
|
||||
source: "info",
|
||||
event: "connected",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
this.$toast.success(this.$t("state.connected"))
|
||||
}
|
||||
this.sse.onerror = () => {
|
||||
this.handleSSEError()
|
||||
}
|
||||
this.sse.onclose = () => {
|
||||
this.connectionSSEState = false
|
||||
addSSELogLine({
|
||||
payload: this.$t("state.disconnected_from", {
|
||||
name: this.server,
|
||||
}),
|
||||
source: "disconnected",
|
||||
event: "disconnected",
|
||||
ts: Date.now(),
|
||||
})
|
||||
this.$toast.error(this.$t("state.disconnected"))
|
||||
}
|
||||
this.sse.addEventListener(this.eventType, ({ data }) => {
|
||||
addSSELogLine({
|
||||
payload: data,
|
||||
source: "server",
|
||||
ts: Date.now(),
|
||||
})
|
||||
})
|
||||
} catch (e) {
|
||||
this.handleSSEError(e)
|
||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||
}
|
||||
} else {
|
||||
this.log = [
|
||||
const t = useI18n()
|
||||
const nuxt = useNuxt()
|
||||
const toast = useToast()
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
const sse = useStream(SSESocket$, new SSEConnection(), setSSESocket)
|
||||
const connectionState = useReadonlyStream(sse.value.connectionState$, "STOPPED")
|
||||
const server = useStream(SSEEndpoint$, "", setSSEEndpoint)
|
||||
const eventType = useStream(SSEEventType$, "", setSSEEventType)
|
||||
const log = useStream(SSELog$, [], setSSELog)
|
||||
|
||||
const isUrlValid = ref(true)
|
||||
|
||||
let worker: Worker
|
||||
|
||||
const debouncer = debounce(function () {
|
||||
worker.postMessage({ type: "sse", url: server.value })
|
||||
}, 1000)
|
||||
|
||||
watch(server, (url) => {
|
||||
if (url) debouncer()
|
||||
})
|
||||
|
||||
const workerResponseHandler = ({
|
||||
data,
|
||||
}: {
|
||||
data: { url: string; result: boolean }
|
||||
}) => {
|
||||
if (data.url === server.value) isUrlValid.value = data.result
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
worker = nuxt.value.$worker.createRejexWorker()
|
||||
worker.addEventListener("message", workerResponseHandler)
|
||||
|
||||
subscribeToStream(sse.value.event$, (event) => {
|
||||
switch (event?.type) {
|
||||
case "STARTING":
|
||||
log.value = [
|
||||
{
|
||||
payload: this.$t("error.browser_support_sse"),
|
||||
source: "disconnected",
|
||||
event: "error",
|
||||
payload: `${t("state.connecting_to", { name: server.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: undefined,
|
||||
},
|
||||
]
|
||||
break
|
||||
|
||||
case "STARTED":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connected_to", { name: server.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
}
|
||||
toast.success(`${t("state.connected")}`)
|
||||
break
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "sse",
|
||||
})
|
||||
},
|
||||
handleSSEError(error) {
|
||||
this.stop()
|
||||
this.connectionSSEState = false
|
||||
addSSELogLine({
|
||||
payload: this.$t("error.something_went_wrong"),
|
||||
source: "disconnected",
|
||||
event: "error",
|
||||
ts: Date.now(),
|
||||
})
|
||||
if (error !== null)
|
||||
case "MESSAGE_RECEIVED":
|
||||
addSSELogLine({
|
||||
payload: error,
|
||||
source: "disconnected",
|
||||
event: "error",
|
||||
ts: Date.now(),
|
||||
payload: event.message,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
},
|
||||
stop() {
|
||||
this.sse.close()
|
||||
this.sse.onclose()
|
||||
},
|
||||
clearLogEntries() {
|
||||
this.log = []
|
||||
},
|
||||
},
|
||||
break
|
||||
|
||||
case "ERROR":
|
||||
addSSELogLine({
|
||||
payload: t("error.browser_support_sse").toString(),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "STOPPED":
|
||||
addSSELogLine({
|
||||
payload: t("state.disconnected_from", {
|
||||
name: server.value,
|
||||
}).toString(),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
toast.error(`${t("state.disconnected")}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// METHODS
|
||||
|
||||
const toggleSSEConnection = () => {
|
||||
// If it is connecting:
|
||||
if (connectionState.value === "STOPPED") {
|
||||
return sse.value.start(server.value, eventType.value)
|
||||
}
|
||||
// Otherwise, it's disconnecting.
|
||||
sse.value.stop()
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
worker.terminate()
|
||||
})
|
||||
const clearLogEntries = () => {
|
||||
log.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -12,133 +12,156 @@
|
||||
type="url"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
:class="{ error: !urlValid }"
|
||||
:placeholder="$t('websocket.url')"
|
||||
:disabled="connectionState"
|
||||
@keyup.enter="urlValid ? toggleConnection() : null"
|
||||
:class="{ error: !isUrlValid }"
|
||||
:placeholder="`${t('websocket.url')}`"
|
||||
:disabled="
|
||||
connectionState === 'CONNECTED' ||
|
||||
connectionState === 'CONNECTING'
|
||||
"
|
||||
@keyup.enter="isUrlValid ? toggleConnection() : null"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="connect"
|
||||
:disabled="!urlValid"
|
||||
:disabled="!isUrlValid"
|
||||
class="w-32"
|
||||
name="connect"
|
||||
:label="
|
||||
!connectionState ? $t('action.connect') : $t('action.disconnect')
|
||||
connectionState === 'DISCONNECTED'
|
||||
? t('action.connect')
|
||||
: t('action.disconnect')
|
||||
"
|
||||
:loading="connectingState"
|
||||
:loading="connectionState === 'CONNECTING'"
|
||||
@click.native="toggleConnection"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||
<SmartTabs
|
||||
v-model="selectedTab"
|
||||
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ $t("websocket.protocols") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.clear_all')"
|
||||
svg="trash-2"
|
||||
@click.native="clearContent"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('add.new')"
|
||||
svg="plus"
|
||||
@click.native="addProtocol"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
v-model="protocols"
|
||||
animation="250"
|
||||
handle=".draggable-handle"
|
||||
draggable=".draggable-content"
|
||||
ghost-class="cursor-move"
|
||||
chosen-class="bg-primaryLight"
|
||||
drag-class="cursor-grabbing"
|
||||
>
|
||||
<div
|
||||
v-for="(protocol, index) of protocols"
|
||||
:key="`protocol-${index}`"
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
<SmartTab
|
||||
:id="'communication'"
|
||||
:label="`${$t('websocket.communication')}`"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
svg="grip-vertical"
|
||||
class="cursor-auto text-primary hover:text-primary"
|
||||
:class="{
|
||||
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
|
||||
index !== protocols?.length - 1,
|
||||
}"
|
||||
tabindex="-1"
|
||||
<RealtimeCommunication
|
||||
:is-connected="connectionState === 'CONNECTED'"
|
||||
@send-message="sendMessage($event)"
|
||||
></RealtimeCommunication>
|
||||
</SmartTab>
|
||||
<SmartTab :id="'protocols'" :label="`${$t('websocket.protocols')}`">
|
||||
<div
|
||||
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
|
||||
>
|
||||
<label class="font-semibold text-secondaryLight">
|
||||
{{ t("websocket.protocols") }}
|
||||
</label>
|
||||
<div class="flex">
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.clear_all')"
|
||||
svg="trash-2"
|
||||
@click.native="clearContent"
|
||||
/>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('add.new')"
|
||||
svg="plus"
|
||||
@click.native="addProtocol"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<draggable
|
||||
v-model="protocols"
|
||||
animation="250"
|
||||
handle=".draggable-handle"
|
||||
draggable=".draggable-content"
|
||||
ghost-class="cursor-move"
|
||||
chosen-class="bg-primaryLight"
|
||||
drag-class="cursor-grabbing"
|
||||
>
|
||||
<div
|
||||
v-for="(protocol, index) of protocols"
|
||||
:key="`protocol-${index}`"
|
||||
class="flex border-b divide-x divide-dividerLight border-dividerLight draggable-content group"
|
||||
>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
svg="grip-vertical"
|
||||
class="cursor-auto text-primary hover:text-primary"
|
||||
:class="{
|
||||
'draggable-handle group-hover:text-secondaryLight !cursor-grab':
|
||||
index !== protocols?.length - 1,
|
||||
}"
|
||||
tabindex="-1"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
v-model="protocol.value"
|
||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||
:placeholder="`${t('count.protocol', { count: index + 1 })}`"
|
||||
name="message"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@change="
|
||||
updateProtocol(index, {
|
||||
value: $event.target.value,
|
||||
active: protocol.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
protocol.hasOwnProperty('active')
|
||||
? protocol.active
|
||||
? t('action.turn_off')
|
||||
: t('action.turn_on')
|
||||
: t('action.turn_off')
|
||||
"
|
||||
:svg="
|
||||
protocol.hasOwnProperty('active')
|
||||
? protocol.active
|
||||
? 'check-circle'
|
||||
: 'circle'
|
||||
: 'check-circle'
|
||||
"
|
||||
color="green"
|
||||
@click.native="
|
||||
updateProtocol(index, {
|
||||
value: protocol.value,
|
||||
active: !protocol.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="deleteProtocol(index)"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="protocols.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_category.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="`${t('empty.protocols')}`"
|
||||
/>
|
||||
</span>
|
||||
<input
|
||||
v-model="protocol.value"
|
||||
class="flex flex-1 px-4 py-2 bg-transparent"
|
||||
:placeholder="$t('count.protocol', { count: index + 1 })"
|
||||
name="message"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
@change="
|
||||
updateProtocol(index, {
|
||||
value: $event.target.value,
|
||||
active: protocol.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="
|
||||
protocol.hasOwnProperty('active')
|
||||
? protocol.active
|
||||
? $t('action.turn_off')
|
||||
: $t('action.turn_on')
|
||||
: $t('action.turn_off')
|
||||
"
|
||||
:svg="
|
||||
protocol.hasOwnProperty('active')
|
||||
? protocol.active
|
||||
? 'check-circle'
|
||||
: 'circle'
|
||||
: 'check-circle'
|
||||
"
|
||||
color="green"
|
||||
@click.native="
|
||||
updateProtocol(index, {
|
||||
value: protocol.value,
|
||||
active: !protocol.active,
|
||||
})
|
||||
"
|
||||
/>
|
||||
</span>
|
||||
<span>
|
||||
<ButtonSecondary
|
||||
v-tippy="{ theme: 'tooltip' }"
|
||||
:title="$t('action.remove')"
|
||||
svg="trash"
|
||||
color="red"
|
||||
@click.native="deleteProtocol({ index })"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</draggable>
|
||||
<div
|
||||
v-if="protocols.length === 0"
|
||||
class="flex flex-col items-center justify-center p-4 text-secondaryLight"
|
||||
>
|
||||
<img
|
||||
:src="`/images/states/${$colorMode.value}/add_category.svg`"
|
||||
loading="lazy"
|
||||
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
|
||||
:alt="$t('empty.protocols')"
|
||||
/>
|
||||
<span class="mb-4 text-center">{{ $t("empty.protocols") }}</span>
|
||||
</div>
|
||||
<span class="mb-4 text-center">
|
||||
{{ t("empty.protocols") }}
|
||||
</span>
|
||||
</div>
|
||||
</SmartTab>
|
||||
</SmartTabs>
|
||||
</template>
|
||||
<template #secondary>
|
||||
<RealtimeLog
|
||||
@@ -147,45 +170,12 @@
|
||||
@delete="clearLogEntries()"
|
||||
/>
|
||||
</template>
|
||||
<template #sidebar>
|
||||
<div class="flex items-center justify-between p-4">
|
||||
<label
|
||||
for="websocket-message"
|
||||
class="font-semibold text-secondaryLight"
|
||||
>{{ $t("websocket.communication") }}</label
|
||||
>
|
||||
</div>
|
||||
<div class="flex px-4 space-x-2">
|
||||
<input
|
||||
id="websocket-message"
|
||||
v-model="communication.input"
|
||||
name="message"
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
:disabled="!connectionState"
|
||||
:placeholder="$t('websocket.message')"
|
||||
class="input"
|
||||
@keyup.enter="connectionState ? sendMessage() : null"
|
||||
@keyup.up="connectionState ? walkHistory('up') : null"
|
||||
@keyup.down="connectionState ? walkHistory('down') : null"
|
||||
/>
|
||||
<ButtonPrimary
|
||||
id="send"
|
||||
name="send"
|
||||
:disabled="!connectionState"
|
||||
:label="$t('action.send')"
|
||||
@click.native="sendMessage"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</AppPaneLayout>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { defineComponent } from "@nuxtjs/composition-api"
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onUnmounted, onMounted } from "@nuxtjs/composition-api"
|
||||
import debounce from "lodash/debounce"
|
||||
import draggable from "vuedraggable"
|
||||
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
|
||||
import {
|
||||
setWSEndpoint,
|
||||
WSEndpoint$,
|
||||
@@ -195,244 +185,185 @@ import {
|
||||
deleteWSProtocol,
|
||||
updateWSProtocol,
|
||||
deleteAllWSProtocols,
|
||||
WSSocket$,
|
||||
setWSSocket,
|
||||
setWSConnectionState,
|
||||
setWSConnectingState,
|
||||
WSConnectionState$,
|
||||
WSConnectingState$,
|
||||
addWSLogLine,
|
||||
WSLog$,
|
||||
setWSLog,
|
||||
HoppWSProtocol,
|
||||
setWSSocket,
|
||||
WSSocket$,
|
||||
} from "~/newstore/WebSocketSession"
|
||||
import { useStream } from "~/helpers/utils/composables"
|
||||
import {
|
||||
useI18n,
|
||||
useStream,
|
||||
useToast,
|
||||
useNuxt,
|
||||
useStreamSubscriber,
|
||||
useReadonlyStream,
|
||||
} from "~/helpers/utils/composables"
|
||||
import { WSConnection, WSErrorMessage } from "~/helpers/realtime/WSConnection"
|
||||
|
||||
export default defineComponent({
|
||||
components: {
|
||||
draggable,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
url: useStream(WSEndpoint$, "", setWSEndpoint),
|
||||
protocols: useStream(WSProtocols$, [], setWSProtocols),
|
||||
connectionState: useStream(
|
||||
WSConnectionState$,
|
||||
false,
|
||||
setWSConnectionState
|
||||
),
|
||||
connectingState: useStream(
|
||||
WSConnectingState$,
|
||||
false,
|
||||
setWSConnectingState
|
||||
),
|
||||
socket: useStream(WSSocket$, null, setWSSocket),
|
||||
log: useStream(WSLog$, [], setWSLog),
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isUrlValid: true,
|
||||
communication: {
|
||||
input: "",
|
||||
},
|
||||
currentIndex: -1, // index of the message log array to put in input box
|
||||
activeProtocols: [],
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
urlValid() {
|
||||
return this.isUrlValid
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
url() {
|
||||
this.debouncer()
|
||||
},
|
||||
protocols: {
|
||||
handler(newVal) {
|
||||
this.activeProtocols = newVal
|
||||
.filter((item) =>
|
||||
Object.prototype.hasOwnProperty.call(item, "active")
|
||||
? item.active === true
|
||||
: true
|
||||
)
|
||||
.map(({ value }) => value)
|
||||
},
|
||||
deep: true,
|
||||
},
|
||||
},
|
||||
created() {
|
||||
if (process.browser) {
|
||||
this.worker = this.$worker.createRejexWorker()
|
||||
this.worker.addEventListener("message", this.workerResponseHandler)
|
||||
}
|
||||
},
|
||||
destroyed() {
|
||||
this.worker.terminate()
|
||||
},
|
||||
methods: {
|
||||
clearContent() {
|
||||
deleteAllWSProtocols()
|
||||
},
|
||||
debouncer: debounce(function () {
|
||||
this.worker.postMessage({ type: "ws", url: this.url })
|
||||
}, 1000),
|
||||
workerResponseHandler({ data }) {
|
||||
if (data.url === this.url) this.isUrlValid = data.result
|
||||
},
|
||||
toggleConnection() {
|
||||
// If it is connecting:
|
||||
if (!this.connectionState) return this.connect()
|
||||
// Otherwise, it's disconnecting.
|
||||
else return this.disconnect()
|
||||
},
|
||||
clearLogEntries() {
|
||||
this.log = []
|
||||
},
|
||||
connect() {
|
||||
this.log = [
|
||||
{
|
||||
payload: this.$t("state.connecting_to", { name: this.url }),
|
||||
source: "info",
|
||||
event: "connecting",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
try {
|
||||
this.connectingState = true
|
||||
this.socket = new WebSocket(this.url, this.activeProtocols)
|
||||
this.socket.onopen = () => {
|
||||
this.connectingState = false
|
||||
this.connectionState = true
|
||||
this.log = [
|
||||
{
|
||||
payload: this.$t("state.connected_to", { name: this.url }),
|
||||
source: "info",
|
||||
event: "connected",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
this.$toast.success(this.$t("state.connected"))
|
||||
}
|
||||
this.socket.onerror = () => {
|
||||
this.handleError()
|
||||
}
|
||||
this.socket.onclose = () => {
|
||||
this.connectionState = false
|
||||
addWSLogLine({
|
||||
payload: this.$t("state.disconnected_from", { name: this.url }),
|
||||
source: "disconnected",
|
||||
event: "disconnected",
|
||||
ts: Date.now(),
|
||||
})
|
||||
this.$toast.error(this.$t("state.disconnected"))
|
||||
}
|
||||
this.socket.onmessage = ({ data }) => {
|
||||
addWSLogLine({
|
||||
payload: data,
|
||||
source: "server",
|
||||
ts: Date.now(),
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
this.handleError(e)
|
||||
this.$toast.error(this.$t("error.something_went_wrong"))
|
||||
}
|
||||
const nuxt = useNuxt()
|
||||
const t = useI18n()
|
||||
const toast = useToast()
|
||||
const { subscribeToStream } = useStreamSubscriber()
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "wss",
|
||||
})
|
||||
},
|
||||
disconnect() {
|
||||
if (this.socket) {
|
||||
this.socket.close()
|
||||
this.connectionState = false
|
||||
this.connectingState = false
|
||||
}
|
||||
},
|
||||
handleError(error) {
|
||||
this.disconnect()
|
||||
this.connectionState = false
|
||||
addWSLogLine({
|
||||
payload: this.$t("error.something_went_wrong"),
|
||||
source: "info",
|
||||
event: "error",
|
||||
ts: Date.now(),
|
||||
})
|
||||
if (error !== null)
|
||||
const selectedTab = ref<"communication" | "protocols">("communication")
|
||||
const url = useStream(WSEndpoint$, "", setWSEndpoint)
|
||||
const protocols = useStream(WSProtocols$, [], setWSProtocols)
|
||||
|
||||
const socket = useStream(WSSocket$, new WSConnection(), setWSSocket)
|
||||
|
||||
const connectionState = useReadonlyStream(
|
||||
socket.value.connectionState$,
|
||||
"DISCONNECTED"
|
||||
)
|
||||
|
||||
const log = useStream(WSLog$, [], setWSLog)
|
||||
// DATA
|
||||
const isUrlValid = ref(true)
|
||||
const activeProtocols = ref<string[]>([])
|
||||
let worker: Worker
|
||||
watch(url, (newUrl) => {
|
||||
if (newUrl) debouncer()
|
||||
})
|
||||
watch(
|
||||
protocols,
|
||||
(newProtocols) => {
|
||||
activeProtocols.value = newProtocols
|
||||
.filter((item) =>
|
||||
Object.prototype.hasOwnProperty.call(item, "active")
|
||||
? item.active === true
|
||||
: true
|
||||
)
|
||||
.map(({ value }) => value)
|
||||
},
|
||||
{ deep: true }
|
||||
)
|
||||
const workerResponseHandler = ({
|
||||
data,
|
||||
}: {
|
||||
data: { url: string; result: boolean }
|
||||
}) => {
|
||||
if (data.url === url.value) isUrlValid.value = data.result
|
||||
}
|
||||
|
||||
const getErrorPayload = (error: WSErrorMessage): string => {
|
||||
if (error instanceof SyntaxError) {
|
||||
return error.message
|
||||
}
|
||||
return t("error.something_went_wrong").toString()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
worker = nuxt.value.$worker.createRejexWorker()
|
||||
worker.addEventListener("message", workerResponseHandler)
|
||||
|
||||
subscribeToStream(socket.value.event$, (event) => {
|
||||
switch (event?.type) {
|
||||
case "CONNECTING":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connecting_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: undefined,
|
||||
},
|
||||
]
|
||||
break
|
||||
|
||||
case "CONNECTED":
|
||||
log.value = [
|
||||
{
|
||||
payload: `${t("state.connected_to", { name: url.value })}`,
|
||||
source: "info",
|
||||
color: "var(--accent-color)",
|
||||
ts: Date.now(),
|
||||
},
|
||||
]
|
||||
toast.success(`${t("state.connected")}`)
|
||||
break
|
||||
|
||||
case "MESSAGE_SENT":
|
||||
addWSLogLine({
|
||||
payload: error,
|
||||
source: "info",
|
||||
event: "error",
|
||||
payload: event.message,
|
||||
source: "client",
|
||||
ts: Date.now(),
|
||||
})
|
||||
},
|
||||
sendMessage() {
|
||||
const message = this.communication.input
|
||||
this.socket.send(message)
|
||||
addWSLogLine({
|
||||
payload: message,
|
||||
source: "client",
|
||||
ts: Date.now(),
|
||||
})
|
||||
this.communication.input = ""
|
||||
},
|
||||
walkHistory(direction) {
|
||||
const clientMessages = this.log.filter(
|
||||
({ source }) => source === "client"
|
||||
)
|
||||
const length = clientMessages.length
|
||||
switch (direction) {
|
||||
case "up":
|
||||
if (length > 0 && this.currentIndex !== 0) {
|
||||
// does nothing if message log is empty or the currentIndex is 0 when up arrow is pressed
|
||||
if (this.currentIndex === -1) {
|
||||
this.currentIndex = length - 1
|
||||
this.communication.input =
|
||||
clientMessages[this.currentIndex].payload
|
||||
} else if (this.currentIndex === 0) {
|
||||
this.communication.input = clientMessages[0].payload
|
||||
} else if (this.currentIndex > 0) {
|
||||
this.currentIndex = this.currentIndex - 1
|
||||
this.communication.input =
|
||||
clientMessages[this.currentIndex].payload
|
||||
}
|
||||
}
|
||||
break
|
||||
case "down":
|
||||
if (length > 0 && this.currentIndex > -1) {
|
||||
if (this.currentIndex === length - 1) {
|
||||
this.currentIndex = -1
|
||||
this.communication.input = ""
|
||||
} else if (this.currentIndex < length - 1) {
|
||||
this.currentIndex = this.currentIndex + 1
|
||||
this.communication.input =
|
||||
clientMessages[this.currentIndex].payload
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
},
|
||||
addProtocol() {
|
||||
addWSProtocol({ value: "", active: true })
|
||||
},
|
||||
deleteProtocol({ index }) {
|
||||
const oldProtocols = this.protocols.slice()
|
||||
deleteWSProtocol(index)
|
||||
this.$toast.success(this.$t("state.deleted"), {
|
||||
action: {
|
||||
text: this.$t("action.undo"),
|
||||
duration: 4000,
|
||||
onClick: (_, toastObject) => {
|
||||
this.protocols = oldProtocols
|
||||
toastObject.remove()
|
||||
},
|
||||
},
|
||||
})
|
||||
},
|
||||
updateProtocol(index, updated) {
|
||||
updateWSProtocol(index, updated)
|
||||
},
|
||||
},
|
||||
break
|
||||
|
||||
case "MESSAGE_RECEIVED":
|
||||
addWSLogLine({
|
||||
payload: event.message,
|
||||
source: "server",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "ERROR":
|
||||
addWSLogLine({
|
||||
payload: getErrorPayload(event.error),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
break
|
||||
|
||||
case "DISCONNECTED":
|
||||
addWSLogLine({
|
||||
payload: t("state.disconnected_from", { name: url.value }).toString(),
|
||||
source: "info",
|
||||
color: "#ff5555",
|
||||
ts: event.time,
|
||||
})
|
||||
toast.error(`${t("state.disconnected")}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (worker) worker.terminate()
|
||||
})
|
||||
const clearContent = () => {
|
||||
deleteAllWSProtocols()
|
||||
}
|
||||
const debouncer = debounce(function () {
|
||||
worker.postMessage({ type: "ws", url: url.value })
|
||||
}, 1000)
|
||||
|
||||
const toggleConnection = () => {
|
||||
// If it is connecting:
|
||||
if (connectionState.value === "DISCONNECTED") {
|
||||
return socket.value.connect(url.value, activeProtocols.value)
|
||||
}
|
||||
// Otherwise, it's disconnecting.
|
||||
socket.value.disconnect()
|
||||
}
|
||||
|
||||
const sendMessage = (event: { message: string; eventName: string }) => {
|
||||
socket.value.sendMessage(event)
|
||||
}
|
||||
const addProtocol = () => {
|
||||
addWSProtocol({ value: "", active: true })
|
||||
}
|
||||
const deleteProtocol = (index: number) => {
|
||||
const oldProtocols = protocols.value.slice()
|
||||
deleteWSProtocol(index)
|
||||
toast.success(`${t("state.deleted")}`, {
|
||||
duration: 4000,
|
||||
action: {
|
||||
text: `${t("action.undo")}`,
|
||||
onClick: (_, toastObject) => {
|
||||
protocols.value = oldProtocols
|
||||
toastObject.goAway()
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
const updateProtocol = (index: number, updated: HoppWSProtocol) => {
|
||||
updateWSProtocol(index, updated)
|
||||
}
|
||||
const clearLogEntries = () => {
|
||||
log.value = []
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -28,8 +28,6 @@ import { javascriptLanguage } from "@codemirror/lang-javascript"
|
||||
import { xmlLanguage } from "@codemirror/lang-xml"
|
||||
import { jsonLanguage } from "@codemirror/lang-json"
|
||||
import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { StreamLanguage } from "@codemirror/stream-parser"
|
||||
import { html } from "@codemirror/legacy-modes/mode/xml"
|
||||
import { shell } from "@codemirror/legacy-modes/mode/shell"
|
||||
@@ -96,8 +94,10 @@ const hoppCompleterExt = (completer: Completer): Extension => {
|
||||
})
|
||||
}
|
||||
|
||||
const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => {
|
||||
const hoppLinterExt = (hoppLinter: LinterDefinition | undefined): Extension => {
|
||||
return linter(async (view) => {
|
||||
if (!hoppLinter) return []
|
||||
|
||||
// Requires full document scan, hence expensive on big files, force disable on big files ?
|
||||
const linterResult = await hoppLinter(
|
||||
view.state.doc.toJSON().join(view.state.lineBreak)
|
||||
@@ -119,16 +119,16 @@ const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => {
|
||||
}
|
||||
|
||||
const hoppLang = (
|
||||
language: Language,
|
||||
language: Language | undefined,
|
||||
linter?: LinterDefinition | undefined,
|
||||
completer?: Completer | undefined
|
||||
) => {
|
||||
): Extension | LanguageSupport => {
|
||||
const exts: Extension[] = []
|
||||
|
||||
if (linter) exts.push(hoppLinterExt(linter))
|
||||
exts.push(hoppLinterExt(linter))
|
||||
if (completer) exts.push(hoppCompleterExt(completer))
|
||||
|
||||
return new LanguageSupport(language, exts)
|
||||
return language ? new LanguageSupport(language, exts) : exts
|
||||
}
|
||||
|
||||
const getLanguage = (langMime: string): Language | null => {
|
||||
@@ -156,12 +156,7 @@ const getEditorLanguage = (
|
||||
langMime: string,
|
||||
linter: LinterDefinition | undefined,
|
||||
completer: Completer | undefined
|
||||
): Extension =>
|
||||
pipe(
|
||||
O.fromNullable(getLanguage(langMime)),
|
||||
O.map((lang) => hoppLang(lang, linter, completer)),
|
||||
O.getOrElseW(() => [])
|
||||
)
|
||||
): Extension => hoppLang(getLanguage(langMime) ?? undefined, linter, completer)
|
||||
|
||||
export function useCodemirror(
|
||||
el: Ref<any | null>,
|
||||
|
||||
223
packages/hoppscotch-app/helpers/realtime/MQTTConnection.ts
Normal file
223
packages/hoppscotch-app/helpers/realtime/MQTTConnection.ts
Normal file
@@ -0,0 +1,223 @@
|
||||
import Paho, { ConnectionOptions } from "paho-mqtt"
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||
|
||||
export type MQTTMessage = { topic: string; message: string }
|
||||
export type MQTTError =
|
||||
| { type: "CONNECTION_NOT_ESTABLISHED"; value: unknown }
|
||||
| { type: "CONNECTION_LOST" }
|
||||
| { type: "CONNECTION_FAILED" }
|
||||
| { type: "SUBSCRIPTION_FAILED"; topic: string }
|
||||
| { type: "PUBLISH_ERROR"; topic: string; message: string }
|
||||
|
||||
export type MQTTEvent = { time: number } & (
|
||||
| { type: "CONNECTING" }
|
||||
| { type: "CONNECTED" }
|
||||
| { type: "MESSAGE_SENT"; message: MQTTMessage }
|
||||
| { type: "SUBSCRIBED"; topic: string }
|
||||
| { type: "SUBSCRIPTION_FAILED"; topic: string }
|
||||
| { type: "MESSAGE_RECEIVED"; message: MQTTMessage }
|
||||
| { type: "DISCONNECTED"; manual: boolean }
|
||||
| { type: "ERROR"; error: MQTTError }
|
||||
)
|
||||
|
||||
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||
|
||||
export class MQTTConnection {
|
||||
subscriptionState$ = new BehaviorSubject<boolean>(false)
|
||||
connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
||||
event$: Subject<MQTTEvent> = new Subject()
|
||||
|
||||
private mqttClient: Paho.Client | undefined
|
||||
private manualDisconnect = false
|
||||
|
||||
private addEvent(event: MQTTEvent) {
|
||||
this.event$.next(event)
|
||||
}
|
||||
|
||||
connect(url: string, username: string, password: string) {
|
||||
try {
|
||||
this.connectionState$.next("CONNECTING")
|
||||
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "CONNECTING",
|
||||
})
|
||||
|
||||
const parseUrl = new URL(url)
|
||||
const { hostname, pathname, port } = parseUrl
|
||||
this.mqttClient = new Paho.Client(
|
||||
`${hostname + (pathname !== "/" ? pathname : "")}`,
|
||||
port !== "" ? Number(port) : 8081,
|
||||
"hoppscotch"
|
||||
)
|
||||
const connectOptions: ConnectionOptions = {
|
||||
onSuccess: this.onConnectionSuccess.bind(this),
|
||||
onFailure: this.onConnectionFailure.bind(this),
|
||||
useSSL: parseUrl.protocol !== "ws:",
|
||||
}
|
||||
if (username !== "") {
|
||||
connectOptions.userName = username
|
||||
}
|
||||
if (password !== "") {
|
||||
connectOptions.password = password
|
||||
}
|
||||
this.mqttClient.connect(connectOptions)
|
||||
this.mqttClient.onConnectionLost = this.onConnectionLost.bind(this)
|
||||
this.mqttClient.onMessageArrived = this.onMessageArrived.bind(this)
|
||||
} catch (e) {
|
||||
this.handleError(e)
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "mqtt",
|
||||
})
|
||||
}
|
||||
|
||||
onConnectionFailure() {
|
||||
this.connectionState$.next("DISCONNECTED")
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error: {
|
||||
type: "CONNECTION_FAILED",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
onConnectionSuccess() {
|
||||
this.connectionState$.next("CONNECTED")
|
||||
this.addEvent({
|
||||
type: "CONNECTED",
|
||||
time: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
onConnectionLost() {
|
||||
this.connectionState$.next("DISCONNECTED")
|
||||
if (this.manualDisconnect) {
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "DISCONNECTED",
|
||||
manual: this.manualDisconnect,
|
||||
})
|
||||
} else {
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error: {
|
||||
type: "CONNECTION_LOST",
|
||||
},
|
||||
})
|
||||
}
|
||||
this.manualDisconnect = false
|
||||
this.subscriptionState$.next(false)
|
||||
}
|
||||
|
||||
onMessageArrived({
|
||||
payloadString: message,
|
||||
destinationName: topic,
|
||||
}: {
|
||||
payloadString: string
|
||||
destinationName: string
|
||||
}) {
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "MESSAGE_RECEIVED",
|
||||
message: {
|
||||
topic,
|
||||
message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
private handleError(error: unknown) {
|
||||
this.disconnect()
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error: {
|
||||
type: "CONNECTION_NOT_ESTABLISHED",
|
||||
value: error,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
publish(topic: string, message: string) {
|
||||
if (this.connectionState$.value === "DISCONNECTED") return
|
||||
|
||||
try {
|
||||
// it was publish
|
||||
this.mqttClient?.send(topic, message, 0, false)
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "MESSAGE_SENT",
|
||||
message: {
|
||||
topic,
|
||||
message,
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error: {
|
||||
type: "PUBLISH_ERROR",
|
||||
topic,
|
||||
message,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
subscribe(topic: string) {
|
||||
try {
|
||||
this.mqttClient?.subscribe(topic, {
|
||||
onSuccess: this.usubSuccess.bind(this, topic),
|
||||
onFailure: this.usubFailure.bind(this, topic),
|
||||
})
|
||||
} catch (e) {
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error: {
|
||||
type: "SUBSCRIPTION_FAILED",
|
||||
topic,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
usubSuccess(topic: string) {
|
||||
this.subscriptionState$.next(!this.subscriptionState$.value)
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "SUBSCRIBED",
|
||||
topic,
|
||||
})
|
||||
}
|
||||
|
||||
usubFailure(topic: string) {
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error: {
|
||||
type: "SUBSCRIPTION_FAILED",
|
||||
topic,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
unsubscribe(topic: string) {
|
||||
this.mqttClient?.unsubscribe(topic, {
|
||||
onSuccess: this.usubSuccess.bind(this, topic),
|
||||
onFailure: this.usubFailure.bind(this, topic),
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.manualDisconnect = true
|
||||
this.mqttClient?.disconnect()
|
||||
this.connectionState$.next("DISCONNECTED")
|
||||
}
|
||||
}
|
||||
84
packages/hoppscotch-app/helpers/realtime/SIOClients.ts
Normal file
84
packages/hoppscotch-app/helpers/realtime/SIOClients.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import wildcard from "socketio-wildcard"
|
||||
import ClientV2 from "socket.io-client-v2"
|
||||
import { io as ClientV4, Socket as SocketV4 } from "socket.io-client-v4"
|
||||
import { io as ClientV3, Socket as SocketV3 } from "socket.io-client-v3"
|
||||
|
||||
type Options = {
|
||||
path: string
|
||||
auth: {
|
||||
token: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
type PossibleEvent =
|
||||
| "connect"
|
||||
| "connect_error"
|
||||
| "reconnect_error"
|
||||
| "error"
|
||||
| "disconnect"
|
||||
| "*"
|
||||
|
||||
export interface SIOClient {
|
||||
connect(url: string, opts?: Options): void
|
||||
on(event: PossibleEvent, cb: (data: any) => void): void
|
||||
emit(event: string, data: any, cb: (data: any) => void): void
|
||||
close(): void
|
||||
}
|
||||
|
||||
export class SIOClientV4 implements SIOClient {
|
||||
private client: SocketV4 | undefined
|
||||
connect(url: string, opts?: Options) {
|
||||
this.client = ClientV4(url, opts)
|
||||
}
|
||||
|
||||
on(event: PossibleEvent, cb: (data: any) => void) {
|
||||
this.client?.on(event, cb)
|
||||
}
|
||||
|
||||
emit(event: string, data: any, cb: (data: any) => void): void {
|
||||
this.client?.emit(event, data, cb)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.client?.close()
|
||||
}
|
||||
}
|
||||
|
||||
export class SIOClientV3 implements SIOClient {
|
||||
private client: SocketV3 | undefined
|
||||
connect(url: string, opts?: Options) {
|
||||
this.client = ClientV3(url, opts)
|
||||
}
|
||||
|
||||
on(event: PossibleEvent, cb: (data: any) => void): void {
|
||||
this.client?.on(event, cb)
|
||||
}
|
||||
|
||||
emit(event: string, data: any, cb: (data: any) => void): void {
|
||||
this.client?.emit(event, data, cb)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.client?.close()
|
||||
}
|
||||
}
|
||||
|
||||
export class SIOClientV2 implements SIOClient {
|
||||
private client: any | undefined
|
||||
connect(url: string, opts?: Options) {
|
||||
this.client = new ClientV2(url, opts)
|
||||
wildcard(ClientV2.Manager)(this.client)
|
||||
}
|
||||
|
||||
on(event: PossibleEvent, cb: (data: any) => void): void {
|
||||
this.client?.on(event, cb)
|
||||
}
|
||||
|
||||
emit(event: string, data: any, cb: (data: any) => void): void {
|
||||
this.client?.emit(event, data, cb)
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.client?.close()
|
||||
}
|
||||
}
|
||||
163
packages/hoppscotch-app/helpers/realtime/SIOConnection.ts
Normal file
163
packages/hoppscotch-app/helpers/realtime/SIOConnection.ts
Normal file
@@ -0,0 +1,163 @@
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||
import { SIOClientV2, SIOClientV3, SIOClientV4, SIOClient } from "./SIOClients"
|
||||
import { SIOClientVersion } from "~/newstore/SocketIOSession"
|
||||
|
||||
export const SOCKET_CLIENTS = {
|
||||
v2: SIOClientV2,
|
||||
v3: SIOClientV3,
|
||||
v4: SIOClientV4,
|
||||
} as const
|
||||
|
||||
type SIOAuth = { type: "None" } | { type: "Bearer"; token: string }
|
||||
|
||||
export type ConnectionOption = {
|
||||
url: string
|
||||
path: string
|
||||
clientVersion: SIOClientVersion
|
||||
auth: SIOAuth | undefined
|
||||
}
|
||||
|
||||
export type SIOMessage = {
|
||||
eventName: string
|
||||
value: unknown
|
||||
}
|
||||
|
||||
type SIOErrorType = "CONNECTION" | "RECONNECT_ERROR" | "UNKNOWN"
|
||||
export type SIOError = {
|
||||
type: SIOErrorType
|
||||
value: unknown
|
||||
}
|
||||
|
||||
export type SIOEvent = { time: number } & (
|
||||
| { type: "CONNECTING" }
|
||||
| { type: "CONNECTED" }
|
||||
| { type: "MESSAGE_SENT"; message: SIOMessage }
|
||||
| { type: "MESSAGE_RECEIVED"; message: SIOMessage }
|
||||
| { type: "DISCONNECTED"; manual: boolean }
|
||||
| { type: "ERROR"; error: SIOError }
|
||||
)
|
||||
|
||||
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||
|
||||
export class SIOConnection {
|
||||
connectionState$: BehaviorSubject<ConnectionState>
|
||||
event$: Subject<SIOEvent> = new Subject()
|
||||
socket: SIOClient | undefined
|
||||
constructor() {
|
||||
this.connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
||||
}
|
||||
|
||||
private addEvent(event: SIOEvent) {
|
||||
this.event$.next(event)
|
||||
}
|
||||
|
||||
connect({ url, path, clientVersion, auth }: ConnectionOption) {
|
||||
this.connectionState$.next("CONNECTING")
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "CONNECTING",
|
||||
})
|
||||
try {
|
||||
this.socket = new SOCKET_CLIENTS[clientVersion]()
|
||||
|
||||
if (auth?.type === "Bearer") {
|
||||
this.socket.connect(url, {
|
||||
path,
|
||||
auth: {
|
||||
token: auth.token,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
this.socket.connect(url)
|
||||
}
|
||||
|
||||
this.socket.on("connect", () => {
|
||||
this.connectionState$.next("CONNECTED")
|
||||
this.addEvent({
|
||||
type: "CONNECTED",
|
||||
time: Date.now(),
|
||||
})
|
||||
})
|
||||
|
||||
this.socket.on("*", ({ data }: { data: string[] }) => {
|
||||
const [eventName, message] = data
|
||||
this.addEvent({
|
||||
message: { eventName, value: message },
|
||||
type: "MESSAGE_RECEIVED",
|
||||
time: Date.now(),
|
||||
})
|
||||
})
|
||||
|
||||
this.socket.on("connect_error", (error: unknown) => {
|
||||
this.handleError(error, "CONNECTION")
|
||||
})
|
||||
|
||||
this.socket.on("reconnect_error", (error: unknown) => {
|
||||
this.handleError(error, "RECONNECT_ERROR")
|
||||
})
|
||||
|
||||
this.socket.on("error", (error: unknown) => {
|
||||
this.handleError(error, "UNKNOWN")
|
||||
})
|
||||
|
||||
this.socket.on("disconnect", () => {
|
||||
this.connectionState$.next("DISCONNECTED")
|
||||
this.addEvent({
|
||||
type: "DISCONNECTED",
|
||||
time: Date.now(),
|
||||
manual: true,
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
this.handleError(error, "CONNECTION")
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "socketio",
|
||||
})
|
||||
}
|
||||
|
||||
private handleError(error: unknown, type: SIOErrorType) {
|
||||
this.disconnect()
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error: {
|
||||
type,
|
||||
value: error,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
sendMessage(event: { message: string; eventName: string }) {
|
||||
if (this.connectionState$.value === "DISCONNECTED") return
|
||||
const { message, eventName } = event
|
||||
|
||||
this.socket?.emit(eventName, message, (data) => {
|
||||
// receive response from server
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "MESSAGE_RECEIVED",
|
||||
message: {
|
||||
eventName,
|
||||
value: data,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "MESSAGE_SENT",
|
||||
message: {
|
||||
eventName,
|
||||
value: message,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.socket?.close()
|
||||
this.connectionState$.next("DISCONNECTED")
|
||||
}
|
||||
}
|
||||
86
packages/hoppscotch-app/helpers/realtime/SSEConnection.ts
Normal file
86
packages/hoppscotch-app/helpers/realtime/SSEConnection.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||
|
||||
export type SSEEvent = { time: number } & (
|
||||
| { type: "STARTING" }
|
||||
| { type: "STARTED" }
|
||||
| { type: "MESSAGE_RECEIVED"; message: string }
|
||||
| { type: "STOPPED"; manual: boolean }
|
||||
| { type: "ERROR"; error: Event | null }
|
||||
)
|
||||
|
||||
export type ConnectionState = "STARTING" | "STARTED" | "STOPPED"
|
||||
|
||||
export class SSEConnection {
|
||||
connectionState$: BehaviorSubject<ConnectionState>
|
||||
event$: Subject<SSEEvent> = new Subject()
|
||||
sse: EventSource | undefined
|
||||
constructor() {
|
||||
this.connectionState$ = new BehaviorSubject<ConnectionState>("STOPPED")
|
||||
}
|
||||
|
||||
private addEvent(event: SSEEvent) {
|
||||
this.event$.next(event)
|
||||
}
|
||||
|
||||
start(url: string, eventType: string) {
|
||||
this.connectionState$.next("STARTING")
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "STARTING",
|
||||
})
|
||||
if (typeof EventSource !== "undefined") {
|
||||
try {
|
||||
this.sse = new EventSource(url)
|
||||
this.sse.onopen = () => {
|
||||
this.connectionState$.next("STARTED")
|
||||
this.addEvent({
|
||||
type: "STARTED",
|
||||
time: Date.now(),
|
||||
})
|
||||
}
|
||||
this.sse.onerror = this.handleError
|
||||
this.sse.addEventListener(eventType, ({ data }) => {
|
||||
this.addEvent({
|
||||
type: "MESSAGE_RECEIVED",
|
||||
message: data,
|
||||
time: Date.now(),
|
||||
})
|
||||
})
|
||||
} catch (error) {
|
||||
// A generic event type returned if anything goes wrong or browser doesn't support SSE
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/EventSource/error_event#event_type
|
||||
this.handleError(error as Event)
|
||||
}
|
||||
} else {
|
||||
this.addEvent({
|
||||
type: "ERROR",
|
||||
time: Date.now(),
|
||||
error: null,
|
||||
})
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "sse",
|
||||
})
|
||||
}
|
||||
|
||||
private handleError(error: Event) {
|
||||
this.stop()
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.sse?.close()
|
||||
this.connectionState$.next("STOPPED")
|
||||
this.addEvent({
|
||||
type: "STOPPED",
|
||||
time: Date.now(),
|
||||
manual: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
102
packages/hoppscotch-app/helpers/realtime/WSConnection.ts
Normal file
102
packages/hoppscotch-app/helpers/realtime/WSConnection.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import { logHoppRequestRunToAnalytics } from "../fb/analytics"
|
||||
|
||||
export type WSErrorMessage = SyntaxError | Event
|
||||
|
||||
export type WSEvent = { time: number } & (
|
||||
| { type: "CONNECTING" }
|
||||
| { type: "CONNECTED" }
|
||||
| { type: "MESSAGE_SENT"; message: string }
|
||||
| { type: "MESSAGE_RECEIVED"; message: string }
|
||||
| { type: "DISCONNECTED"; manual: boolean }
|
||||
| { type: "ERROR"; error: WSErrorMessage }
|
||||
)
|
||||
|
||||
export type ConnectionState = "CONNECTING" | "CONNECTED" | "DISCONNECTED"
|
||||
|
||||
export class WSConnection {
|
||||
connectionState$: BehaviorSubject<ConnectionState>
|
||||
event$: Subject<WSEvent> = new Subject()
|
||||
socket: WebSocket | undefined
|
||||
|
||||
constructor() {
|
||||
this.connectionState$ = new BehaviorSubject<ConnectionState>("DISCONNECTED")
|
||||
}
|
||||
|
||||
private addEvent(event: WSEvent) {
|
||||
this.event$.next(event)
|
||||
}
|
||||
|
||||
connect(url: string, protocols: string[]) {
|
||||
try {
|
||||
this.connectionState$.next("CONNECTING")
|
||||
this.socket = new WebSocket(url, protocols)
|
||||
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "CONNECTING",
|
||||
})
|
||||
|
||||
this.socket.onopen = () => {
|
||||
this.connectionState$.next("CONNECTED")
|
||||
this.addEvent({
|
||||
type: "CONNECTED",
|
||||
time: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
this.socket.onerror = (error) => {
|
||||
this.handleError(error)
|
||||
}
|
||||
|
||||
this.socket.onclose = () => {
|
||||
this.connectionState$.next("DISCONNECTED")
|
||||
this.addEvent({
|
||||
type: "DISCONNECTED",
|
||||
time: Date.now(),
|
||||
manual: true,
|
||||
})
|
||||
}
|
||||
|
||||
this.socket.onmessage = ({ data }) => {
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "MESSAGE_RECEIVED",
|
||||
message: data,
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
// We will have SyntaxError if anything goes wrong with WebSocket constructor
|
||||
// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket#exceptions
|
||||
this.handleError(error as SyntaxError)
|
||||
}
|
||||
|
||||
logHoppRequestRunToAnalytics({
|
||||
platform: "wss",
|
||||
})
|
||||
}
|
||||
|
||||
private handleError(error: WSErrorMessage) {
|
||||
this.disconnect()
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "ERROR",
|
||||
error,
|
||||
})
|
||||
}
|
||||
|
||||
sendMessage(event: { message: string; eventName: string }) {
|
||||
if (this.connectionState$.value === "DISCONNECTED") return
|
||||
const { message } = event
|
||||
this.socket?.send(message)
|
||||
this.addEvent({
|
||||
time: Date.now(),
|
||||
type: "MESSAGE_SENT",
|
||||
message,
|
||||
})
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.socket?.close()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
export type HoppRealtimeLogLine = {
|
||||
prefix?: string
|
||||
payload: string
|
||||
source: string
|
||||
color?: string
|
||||
ts: string
|
||||
ts: number | undefined
|
||||
}
|
||||
|
||||
export type HoppRealtimeLog = HoppRealtimeLogLine[]
|
||||
|
||||
@@ -508,7 +508,8 @@
|
||||
"event_name": "Event Name",
|
||||
"events": "Events",
|
||||
"log": "Log",
|
||||
"url": "URL"
|
||||
"url": "URL",
|
||||
"connection_not_authorized": "This SocketIO connection does not use any authentication."
|
||||
},
|
||||
"sse": {
|
||||
"event_type": "Event type",
|
||||
@@ -538,7 +539,19 @@
|
||||
"loading": "Loading...",
|
||||
"none": "None",
|
||||
"nothing_found": "Nothing found for",
|
||||
"waiting_send_request": "Waiting to send request"
|
||||
"waiting_send_request": "Waiting to send request",
|
||||
"subscribed_success": "Successfully subscribed to topic: {topic}",
|
||||
"unsubscribed_success": "Successfully unsubscribed from topic: {topic}",
|
||||
"subscribed_failed": "Failed to subscribe to topic: {topic}",
|
||||
"unsubscribed_failed": "Failed to unsubscribe from topic: {topic}",
|
||||
"published_message": "Published message: {message} to topic: {topic}",
|
||||
"published_error": "Something went wrong while publishing msg: {topic} to topic: {message}",
|
||||
"message_received": "Message: {message} arrived on topic: {topic}",
|
||||
"mqtt_subscription_failed": "Something went wrong while subscribing to topic: {topic}",
|
||||
"connection_lost": "Connection lost",
|
||||
"connection_failed": "Connection failed",
|
||||
"connection_error": "Failed to connect",
|
||||
"reconnection_error": "Failed to reconnect"
|
||||
},
|
||||
"support": {
|
||||
"changelog": "Read more about latest releases",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { pluck, distinctUntilChanged } from "rxjs/operators"
|
||||
import { Client as MQTTClient } from "paho-mqtt"
|
||||
import { distinctUntilChanged, pluck } from "rxjs/operators"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { MQTTConnection } from "~/helpers/realtime/MQTTConnection"
|
||||
import {
|
||||
HoppRealtimeLog,
|
||||
HoppRealtimeLogLine,
|
||||
@@ -12,11 +12,9 @@ type HoppMQTTRequest = {
|
||||
|
||||
type HoppMQTTSession = {
|
||||
request: HoppMQTTRequest
|
||||
connectingState: boolean
|
||||
connectionState: boolean
|
||||
subscriptionState: boolean
|
||||
log: HoppRealtimeLog
|
||||
socket: MQTTClient | null
|
||||
socket: MQTTConnection
|
||||
}
|
||||
|
||||
const defaultMQTTRequest: HoppMQTTRequest = {
|
||||
@@ -25,10 +23,8 @@ const defaultMQTTRequest: HoppMQTTRequest = {
|
||||
|
||||
const defaultMQTTSession: HoppMQTTSession = {
|
||||
request: defaultMQTTRequest,
|
||||
connectionState: false,
|
||||
connectingState: false,
|
||||
subscriptionState: false,
|
||||
socket: null,
|
||||
socket: new MQTTConnection(),
|
||||
log: [],
|
||||
}
|
||||
|
||||
@@ -48,21 +44,11 @@ const dispatchers = defineDispatchers({
|
||||
},
|
||||
}
|
||||
},
|
||||
setSocket(_: HoppMQTTSession, { socket }: { socket: MQTTClient }) {
|
||||
setConn(_: HoppMQTTSession, { socket }: { socket: MQTTConnection }) {
|
||||
return {
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setConnectionState(_: HoppMQTTSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectionState: state,
|
||||
}
|
||||
},
|
||||
setConnectingState(_: HoppMQTTSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectingState: state,
|
||||
}
|
||||
},
|
||||
setSubscriptionState(_: HoppMQTTSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
subscriptionState: state,
|
||||
@@ -100,33 +86,15 @@ export function setMQTTEndpoint(newEndpoint: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTSocket(socket: MQTTClient) {
|
||||
export function setMQTTConn(socket: MQTTConnection) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setSocket",
|
||||
dispatcher: "setConn",
|
||||
payload: {
|
||||
socket,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTConnectionState(state: boolean) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setConnectionState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTConnectingState(state: boolean) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setConnectingState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTSubscriptionState(state: boolean) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setSubscriptionState",
|
||||
@@ -179,7 +147,7 @@ export const MQTTSubscriptionState$ = MQTTSessionStore.subject$.pipe(
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTSocket$ = MQTTSessionStore.subject$.pipe(
|
||||
export const MQTTConn$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("socket"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
HoppRealtimeLog,
|
||||
HoppRealtimeLogLine,
|
||||
} from "~/helpers/types/HoppRealtimeLog"
|
||||
import { SSEConnection } from "~/helpers/realtime/SSEConnection"
|
||||
|
||||
type HoppSSERequest = {
|
||||
endpoint: string
|
||||
@@ -12,10 +13,8 @@ type HoppSSERequest = {
|
||||
|
||||
type HoppSSESession = {
|
||||
request: HoppSSERequest
|
||||
connectingState: boolean
|
||||
connectionState: boolean
|
||||
log: HoppRealtimeLog
|
||||
socket: EventSource | null
|
||||
socket: SSEConnection
|
||||
}
|
||||
|
||||
const defaultSSERequest: HoppSSERequest = {
|
||||
@@ -25,9 +24,7 @@ const defaultSSERequest: HoppSSERequest = {
|
||||
|
||||
const defaultSSESession: HoppSSESession = {
|
||||
request: defaultSSERequest,
|
||||
connectionState: false,
|
||||
connectingState: false,
|
||||
socket: null,
|
||||
socket: new SSEConnection(),
|
||||
log: [],
|
||||
}
|
||||
|
||||
@@ -56,21 +53,11 @@ const dispatchers = defineDispatchers({
|
||||
},
|
||||
}
|
||||
},
|
||||
setSocket(_: HoppSSESession, { socket }: { socket: EventSource }) {
|
||||
setSocket(_: HoppSSESession, { socket }: { socket: SSEConnection }) {
|
||||
return {
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setConnectionState(_: HoppSSESession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectionState: state,
|
||||
}
|
||||
},
|
||||
setConnectingState(_: HoppSSESession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectingState: state,
|
||||
}
|
||||
},
|
||||
setLog(_: HoppSSESession, { log }: { log: HoppRealtimeLog }) {
|
||||
return {
|
||||
log,
|
||||
@@ -112,7 +99,7 @@ export function setSSEEventType(newType: string) {
|
||||
})
|
||||
}
|
||||
|
||||
export function setSSESocket(socket: EventSource) {
|
||||
export function setSSESocket(socket: SSEConnection) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setSocket",
|
||||
payload: {
|
||||
@@ -121,23 +108,6 @@ export function setSSESocket(socket: EventSource) {
|
||||
})
|
||||
}
|
||||
|
||||
export function setSSEConnectionState(state: boolean) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setConnectionState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
export function setSSEConnectingState(state: boolean) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setConnectingState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSSELog(log: HoppRealtimeLog) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setLog",
|
||||
@@ -176,11 +146,6 @@ export const SSEConnectingState$ = SSESessionStore.subject$.pipe(
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SSEConnectionState$ = SSESessionStore.subject$.pipe(
|
||||
pluck("connectionState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SSESocket$ = SSESessionStore.subject$.pipe(
|
||||
pluck("socket"),
|
||||
distinctUntilChanged()
|
||||
|
||||
@@ -10,16 +10,16 @@ import {
|
||||
|
||||
type SocketIO = SocketV2 | SocketV3 | SocketV4
|
||||
|
||||
export type SIOClientVersion = "v4" | "v3" | "v2"
|
||||
|
||||
type HoppSIORequest = {
|
||||
endpoint: string
|
||||
path: string
|
||||
version: string
|
||||
version: SIOClientVersion
|
||||
}
|
||||
|
||||
type HoppSIOSession = {
|
||||
request: HoppSIORequest
|
||||
connectingState: boolean
|
||||
connectionState: boolean
|
||||
log: HoppRealtimeLog
|
||||
socket: SocketIO | null
|
||||
}
|
||||
@@ -32,8 +32,6 @@ const defaultSIORequest: HoppSIORequest = {
|
||||
|
||||
const defaultSIOSession: HoppSIOSession = {
|
||||
request: defaultSIORequest,
|
||||
connectionState: false,
|
||||
connectingState: false,
|
||||
socket: null,
|
||||
log: [],
|
||||
}
|
||||
@@ -63,7 +61,10 @@ const dispatchers = defineDispatchers({
|
||||
},
|
||||
}
|
||||
},
|
||||
setVersion(curr: HoppSIOSession, { newVersion }: { newVersion: string }) {
|
||||
setVersion(
|
||||
curr: HoppSIOSession,
|
||||
{ newVersion }: { newVersion: SIOClientVersion }
|
||||
) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
@@ -76,16 +77,6 @@ const dispatchers = defineDispatchers({
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setConnectionState(_: HoppSIOSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectionState: state,
|
||||
}
|
||||
},
|
||||
setConnectingState(_: HoppSIOSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectingState: state,
|
||||
}
|
||||
},
|
||||
setLog(_: HoppSIOSession, { log }: { log: HoppRealtimeLog }) {
|
||||
return {
|
||||
log,
|
||||
@@ -145,23 +136,6 @@ export function setSIOSocket(socket: SocketIO) {
|
||||
})
|
||||
}
|
||||
|
||||
export function setSIOConnectionState(state: boolean) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setConnectionState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
export function setSIOConnectingState(state: boolean) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setConnectingState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSIOLog(log: HoppRealtimeLog) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setLog",
|
||||
@@ -200,11 +174,6 @@ export const SIOPath$ = SIOSessionStore.subject$.pipe(
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOConnectingState$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("connectingState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOConnectionState$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("connectionState"),
|
||||
distinctUntilChanged()
|
||||
|
||||
@@ -4,8 +4,9 @@ import {
|
||||
HoppRealtimeLog,
|
||||
HoppRealtimeLogLine,
|
||||
} from "~/helpers/types/HoppRealtimeLog"
|
||||
import { WSConnection } from "~/helpers/realtime/WSConnection"
|
||||
|
||||
type HoppWSProtocol = {
|
||||
export type HoppWSProtocol = {
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
@@ -17,10 +18,8 @@ type HoppWSRequest = {
|
||||
|
||||
export type HoppWSSession = {
|
||||
request: HoppWSRequest
|
||||
connectingState: boolean
|
||||
connectionState: boolean
|
||||
log: HoppRealtimeLog
|
||||
socket: WebSocket | null
|
||||
socket: WSConnection
|
||||
}
|
||||
|
||||
const defaultWSRequest: HoppWSRequest = {
|
||||
@@ -30,9 +29,7 @@ const defaultWSRequest: HoppWSRequest = {
|
||||
|
||||
const defaultWSSession: HoppWSSession = {
|
||||
request: defaultWSRequest,
|
||||
connectionState: false,
|
||||
connectingState: false,
|
||||
socket: null,
|
||||
socket: new WSConnection(),
|
||||
log: [],
|
||||
}
|
||||
|
||||
@@ -101,21 +98,11 @@ const dispatchers = defineDispatchers({
|
||||
},
|
||||
}
|
||||
},
|
||||
setSocket(_: HoppWSSession, { socket }: { socket: WebSocket }) {
|
||||
setSocket(_: HoppWSSession, { socket }: { socket: WSConnection }) {
|
||||
return {
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setConnectionState(_: HoppWSSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectionState: state,
|
||||
}
|
||||
},
|
||||
setConnectingState(_: HoppWSSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
connectingState: state,
|
||||
}
|
||||
},
|
||||
setLog(_: HoppWSSession, { log }: { log: HoppRealtimeLog }) {
|
||||
return {
|
||||
log,
|
||||
@@ -195,7 +182,7 @@ export function updateWSProtocol(
|
||||
})
|
||||
}
|
||||
|
||||
export function setWSSocket(socket: WebSocket) {
|
||||
export function setWSSocket(socket: WSConnection) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setSocket",
|
||||
payload: {
|
||||
@@ -204,23 +191,6 @@ export function setWSSocket(socket: WebSocket) {
|
||||
})
|
||||
}
|
||||
|
||||
export function setWSConnectionState(state: boolean) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setConnectionState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
export function setWSConnectingState(state: boolean) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setConnectingState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setWSLog(log: HoppRealtimeLog) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setLog",
|
||||
|
||||
@@ -153,6 +153,7 @@
|
||||
"@types/paho-mqtt": "^1.0.6",
|
||||
"@types/postman-collection": "^3.5.7",
|
||||
"@types/qs": "^6.9.7",
|
||||
"@types/socketio-wildcard": "^2.0.4",
|
||||
"@types/splitpanes": "^2.2.1",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"@types/yargs-parser": "^21.0.0",
|
||||
|
||||
19
packages/hoppscotch-app/types/socket-io-2.d.ts
vendored
19
packages/hoppscotch-app/types/socket-io-2.d.ts
vendored
@@ -2,6 +2,21 @@
|
||||
// tsc, this is really annoying (and maybe dangerous)
|
||||
// We don't have access to the 2.4.0 typings, hence we make do with this,
|
||||
// Check docs before you correct types again as you need
|
||||
declare module "socket.io-client-v2" {
|
||||
export type Socket = any
|
||||
|
||||
type Options = {
|
||||
path: string
|
||||
auth: {
|
||||
token: string | undefined
|
||||
}
|
||||
}
|
||||
|
||||
declare module "socket.io-client-v2" {
|
||||
export type Socket = unknown
|
||||
export default class ClientV2 {
|
||||
static Manager: { prototype: EventEmitter } | undefined
|
||||
constructor(url: string, opts?: Options)
|
||||
on(event: string, cb: (data: any) => void): void
|
||||
emit(event: string, data: any, cb: (data: any) => void): void
|
||||
close(): void
|
||||
}
|
||||
}
|
||||
|
||||
552
pnpm-lock.yaml
generated
552
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user