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:
Anwarul Islam
2022-05-28 15:35:41 +06:00
committed by GitHub
parent 83bdd03f43
commit f6950bac0f
24 changed files with 2138 additions and 1819 deletions

View 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

View File

@@ -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>

View 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>

View File

@@ -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"

View File

@@ -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)

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>,

View 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")
}
}

View 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()
}
}

View 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")
}
}

View 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,
})
}
}

View 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()
}
}

View File

@@ -1,8 +1,9 @@
export type HoppRealtimeLogLine = {
prefix?: string
payload: string
source: string
color?: string
ts: string
ts: number | undefined
}
export type HoppRealtimeLog = HoppRealtimeLogLine[]

View File

@@ -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",

View File

@@ -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()
)

View File

@@ -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()

View File

@@ -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()

View File

@@ -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",

View File

@@ -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",

View File

@@ -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

File diff suppressed because it is too large Load Diff