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> </Splitpanes>
</Pane> </Pane>
<Pane <Pane
v-if="SIDEBAR" v-if="SIDEBAR && hasSidebar"
size="25" size="25"
min-size="20" min-size="20"
class="hide-scrollbar !overflow-auto flex flex-col" class="hide-scrollbar !overflow-auto flex flex-col"
@@ -42,6 +42,7 @@
import { Splitpanes, Pane } from "splitpanes" import { Splitpanes, Pane } from "splitpanes"
import "splitpanes/dist/splitpanes.css" import "splitpanes/dist/splitpanes.css"
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core" import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"
import { computed, useSlots } from "@nuxtjs/composition-api"
import { useSetting } from "~/newstore/settings" import { useSetting } from "~/newstore/settings"
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT") const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
@@ -52,4 +53,8 @@ const mdAndLarger = breakpoints.greater("md")
const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT") const COLUMN_LAYOUT = useSetting("COLUMN_LAYOUT")
const SIDEBAR = useSetting("SIDEBAR") const SIDEBAR = useSetting("SIDEBAR")
const slots = useSlots()
const hasSidebar = computed(() => !!slots.sidebar)
</script> </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" import { useI18n } from "~/helpers/utils/composables"
export type LogEntryData = { export type LogEntryData = {
ts: number prefix?: string
ts: number | undefined
source: "info" | "client" | "server" | "disconnected" source: "info" | "client" | "server" | "disconnected"
payload: string payload: string
event: "connecting" | "connected" | "disconnected" | "error" event: "connecting" | "connected" | "disconnected" | "error"

View File

@@ -11,7 +11,10 @@
@click.native="copyQuery(entry.payload)" @click.native="copyQuery(entry.payload)"
/> />
</div> </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 <span
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="relativeTime" :title="relativeTime"
@@ -25,6 +28,9 @@
@click="toggleExpandPayload()" @click="toggleExpandPayload()"
> >
<div class="truncate"> <div class="truncate">
<span v-if="entry.prefix !== undefined" class="!inline">{{
entry.prefix
}}</span>
{{ entry.payload }} {{ entry.payload }}
</div> </div>
</div> </div>
@@ -312,26 +318,20 @@ const copyQuery = (entry: string) => {
} }
// Relative Time // 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 // Assigns color based on entry event
const entryColor = computed(() => { const entryColor = computed(() => ENTRY_COLORS[props.entry.event])
switch (props.entry.event) {
case "connected":
return "#10b981"
case "connecting":
return "#10b981"
case "error":
return "#ff5555"
case "disconnected":
return "#ff5555"
}
})
const ICONS: Record< const ICONS = {
LogEntryData["source"],
{ iconName: string; iconColor: string }
> = {
info: { info: {
iconName: "info-realtime", iconName: "info-realtime",
iconColor: "#10b981", iconColor: "#10b981",
@@ -348,7 +348,7 @@ const ICONS: Record<
iconName: "info-disconnect", iconName: "info-disconnect",
iconColor: "#ff5555", iconColor: "#ff5555",
}, },
} } as const
const iconColor = computed(() => ICONS[props.entry.source].iconColor) const iconColor = computed(() => ICONS[props.entry.source].iconColor)
const iconName = computed(() => ICONS[props.entry.source].iconName) const iconName = computed(() => ICONS[props.entry.source].iconName)

View File

@@ -13,17 +13,22 @@
spellcheck="false" spellcheck="false"
class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark" class="w-full px-4 py-2 border rounded bg-primaryLight border-divider text-secondaryDark"
:placeholder="$t('mqtt.url')" :placeholder="$t('mqtt.url')"
:disabled="connectionState" :disabled="
@keyup.enter="validUrl ? toggleConnection() : null" connectionState === 'CONNECTED' ||
connectionState === 'CONNECTING'
"
@keyup.enter="isUrlValid ? toggleConnection() : null"
/> />
<ButtonPrimary <ButtonPrimary
id="connect" id="connect"
:disabled="!validUrl" :disabled="!isUrlValid"
class="w-32" class="w-32"
:label=" :label="
connectionState ? $t('action.disconnect') : $t('action.connect') connectionState === 'DISCONNECTED'
? t('action.connect')
: t('action.disconnect')
" "
:loading="connectingState" :loading="connectionState === 'CONNECTING'"
@click.native="toggleConnection" @click.native="toggleConnection"
/> />
</div> </div>
@@ -56,14 +61,14 @@
</template> </template>
<template #sidebar> <template #sidebar>
<div class="flex items-center justify-between p-4"> <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") }} {{ $t("mqtt.topic") }}
</label> </label>
</div> </div>
<div class="flex px-4"> <div class="flex px-4">
<input <input
id="pub_topic" id="pubTopic"
v-model="pub_topic" v-model="pubTopic"
class="input" class="input"
:placeholder="$t('mqtt.topic_name')" :placeholder="$t('mqtt.topic_name')"
type="text" type="text"
@@ -79,7 +84,7 @@
<div class="flex px-4 space-x-2"> <div class="flex px-4 space-x-2">
<input <input
id="mqtt-message" id="mqtt-message"
v-model="msg" v-model="message"
class="input" class="input"
type="text" type="text"
autocomplete="off" autocomplete="off"
@@ -89,7 +94,7 @@
<ButtonPrimary <ButtonPrimary
id="publish" id="publish"
name="get" name="get"
:disabled="!canpublish" :disabled="!canPublish"
:label="$t('mqtt.publish')" :label="$t('mqtt.publish')"
@click.native="publish" @click.native="publish"
/> />
@@ -97,14 +102,14 @@
<div <div
class="flex items-center justify-between p-4 mt-4 border-t border-dividerLight" 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") }} {{ $t("mqtt.topic") }}
</label> </label>
</div> </div>
<div class="flex px-4 space-x-2"> <div class="flex px-4 space-x-2">
<input <input
id="sub_topic" id="subTopic"
v-model="sub_topic" v-model="subTopic"
type="text" type="text"
autocomplete="off" autocomplete="off"
:placeholder="$t('mqtt.topic_name')" :placeholder="$t('mqtt.topic_name')"
@@ -114,7 +119,7 @@
<ButtonPrimary <ButtonPrimary
id="subscribe" id="subscribe"
name="get" name="get"
:disabled="!cansubscribe" :disabled="!canSubscribe"
:label=" :label="
subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe') subscriptionState ? $t('mqtt.unsubscribe') : $t('mqtt.subscribe')
" "
@@ -126,264 +131,220 @@
</AppPaneLayout> </AppPaneLayout>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import {
import Paho from "paho-mqtt" computed,
import debounce from "lodash/debounce" onMounted,
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics" 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 { import {
MQTTEndpoint$,
setMQTTEndpoint,
MQTTConnectingState$,
MQTTConnectionState$,
setMQTTConnectingState,
setMQTTConnectionState,
MQTTSubscriptionState$,
setMQTTSubscriptionState,
MQTTSocket$,
setMQTTSocket,
MQTTLog$,
setMQTTLog,
addMQTTLogLine, addMQTTLogLine,
MQTTConn$,
MQTTEndpoint$,
MQTTLog$,
setMQTTConn,
setMQTTEndpoint,
setMQTTLog,
} from "~/newstore/MQTTSession" } from "~/newstore/MQTTSession"
import { useStream } from "~/helpers/utils/composables"
export default defineComponent({ const t = useI18n()
setup() { const nuxt = useNuxt()
return { const toast = useToast()
url: useStream(MQTTEndpoint$, "", setMQTTEndpoint), const { subscribeToStream } = useStreamSubscriber()
connectionState: useStream(
MQTTConnectionState$, const url = useStream(MQTTEndpoint$, "", setMQTTEndpoint)
false, const log = useStream(MQTTLog$, [], setMQTTLog)
setMQTTConnectionState const socket = useStream(MQTTConn$, new MQTTConnection(), setMQTTConn)
), const connectionState = useReadonlyStream(
connectingState: useStream( socket.value.connectionState$,
MQTTConnectingState$, "DISCONNECTED"
false, )
setMQTTConnectingState const subscriptionState = useReadonlyStream(
), socket.value.subscriptionState$,
subscriptionState: useStream( false
MQTTSubscriptionState$, )
false,
setMQTTSubscriptionState const isUrlValid = ref(true)
), const pubTopic = ref("")
log: useStream(MQTTLog$, null, setMQTTLog), const subTopic = ref("")
client: useStream(MQTTSocket$, null, setMQTTSocket), const message = ref("")
} const username = ref("")
}, const password = ref("")
data() {
return { let worker: Worker
isUrlValid: true,
pub_topic: "", const canPublish = computed(
sub_topic: "", () =>
msg: "", pubTopic.value !== "" &&
manualDisconnect: false, message.value !== "" &&
username: "", connectionState.value === "CONNECTED"
password: "", )
} const canSubscribe = computed(
}, () => subTopic.value !== "" && connectionState.value === "CONNECTED"
computed: { )
validUrl() {
return this.isUrlValid const workerResponseHandler = ({
}, data,
canpublish() { }: {
return this.pub_topic !== "" && this.msg !== "" && this.connectionState data: { url: string; result: boolean }
}, }) => {
cansubscribe() { if (data.url === url.value) isUrlValid.value = data.result
return this.sub_topic !== "" && this.connectionState }
},
}, onMounted(() => {
watch: { worker = nuxt.value.$worker.createRejexWorker()
url() { worker.addEventListener("message", workerResponseHandler)
this.debouncer()
}, subscribeToStream(socket.value.event$, (event) => {
}, switch (event?.type) {
created() { case "CONNECTING":
if (process.browser) { log.value = [
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 }), payload: `${t("state.connecting_to", { name: url.value })}`,
source: "info", source: "info",
event: "connecting", 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(), ts: Date.now(),
}, },
] ]
const parseUrl = new URL(this.url) toast.success(`${t("state.connected")}`)
this.client = new Paho.Client( break
`${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
logHoppRequestRunToAnalytics({ case "MESSAGE_SENT":
platform: "mqtt",
})
},
onConnectionFailure() {
this.connectingState = false
this.connectionState = false
addMQTTLogLine({ addMQTTLogLine({
payload: this.$t("error.something_went_wrong"), prefix: `${event.message.topic}`,
source: "info", payload: event.message.message,
event: "error", source: "client",
ts: Date.now(), ts: Date.now(),
}) })
}, break
onConnectionSuccess() {
this.connectingState = false case "MESSAGE_RECEIVED":
this.connectionState = true
addMQTTLogLine({ addMQTTLogLine({
payload: this.$t("state.connected_to", { name: this.url }), prefix: `${event.message.topic}`,
source: "info", payload: event.message.message,
event: "connected", source: "server",
ts: Date.now(), ts: event.time,
}) })
this.$toast.success(this.$t("state.connected")) break
},
onMessageArrived({ payloadString, destinationName }) { case "SUBSCRIBED":
addMQTTLogLine({ addMQTTLogLine({
payload: `Message: ${payloadString} arrived on topic: ${destinationName}`, payload: subscriptionState.value
source: "info", ? `${t("state.subscribed_success", { topic: subTopic.value })}`
event: "info", : `${t("state.unsubscribed_success", { topic: subTopic.value })}`,
ts: Date.now(), source: "server",
ts: event.time,
}) })
}, break
toggleConnection() {
if (this.connectionState) { case "SUBSCRIPTION_FAILED":
this.disconnect() addMQTTLogLine({
} else { payload: subscriptionState.value
this.connect() ? `${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
} }
},
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)
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",
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) {
addMQTTLogLine({
payload:
this.$t("error.something_went_wrong") +
`while subscribing to topic: ${this.sub_topic}`,
source: "info",
event: "error",
ts: Date.now(),
})
}
},
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 = []
},
},
}) })
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> </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" class="flex px-4 py-2 font-semibold border rounded-l cursor-pointer bg-primaryLight border-divider text-secondaryDark w-26"
:value="`Client ${clientVersion}`" :value="`Client ${clientVersion}`"
readonly readonly
:disabled="connectionState" :disabled="
connectionState === 'CONNECTED' ||
connectionState === 'CONNECTING'
"
/> />
</span> </span>
</template> </template>
<div class="flex flex-col" role="menu"> <div class="flex flex-col" role="menu">
<SmartItem <SmartItem
v-for="(_, version) in socketIoClients" v-for="version in SIOVersions"
:key="`client-${version}`" :key="`client-${version}`"
:label="`Client ${version}`" :label="`Client ${version}`"
@click.native="onSelectVersion(version)" @click.native="onSelectVersion(version)"
@@ -43,40 +46,64 @@
type="url" type="url"
autocomplete="off" autocomplete="off"
spellcheck="false" 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" class="flex flex-1 w-full px-4 py-2 border bg-primaryLight border-divider text-secondaryDark"
:placeholder="$t('socketio.url')" :placeholder="`${t('socketio.url')}`"
:disabled="connectionState" :disabled="
@keyup.enter="urlValid ? toggleConnection() : null" connectionState === 'CONNECTED' ||
connectionState === 'CONNECTING'
"
@keyup.enter="isUrlValid ? toggleConnection() : null"
/> />
<input <input
id="socketio-path" id="socketio-path"
v-model="path" v-model="path"
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark" class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
spellcheck="false" spellcheck="false"
:disabled="connectionState" :disabled="
@keyup.enter="urlValid ? toggleConnection() : null" connectionState === 'CONNECTED' ||
connectionState === 'CONNECTING'
"
@keyup.enter="isUrlValid ? toggleConnection() : null"
/> />
</div> </div>
<ButtonPrimary <ButtonPrimary
id="connect" id="connect"
:disabled="!urlValid" :disabled="!isUrlValid"
name="connect" name="connect"
class="w-32" class="w-32"
:label=" :label="
!connectionState ? $t('action.connect') : $t('action.disconnect') connectionState === 'DISCONNECTED'
? t('action.connect')
: t('action.disconnect')
" "
:loading="connectingState" :loading="connectionState === 'CONNECTING'"
@click.native="toggleConnection" @click.native="toggleConnection"
/> />
</div> </div>
</div> </div>
<SmartTabs
v-model="selectedTab"
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
>
<SmartTab
:id="'communication'"
:label="`${t('websocket.communication')}`"
>
<RealtimeCommunication
:show-event-field="true"
:is-connected="connectionState === 'CONNECTED'"
@send-message="sendMessage($event)"
></RealtimeCommunication>
</SmartTab>
<SmartTab :id="'protocols'" :label="`${t('request.authorization')}`">
<div <div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold" class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold"
> >
<span class="flex items-center"> <span class="flex items-center">
<label class="font-semibold text-secondaryLight"> <label class="font-semibold text-secondaryLight">
{{ $t("authorization.type") }} {{ t("authorization.type") }}
</label> </label>
<tippy <tippy
ref="authTypeOptions" ref="authTypeOptions"
@@ -133,18 +160,18 @@
class="px-2" class="px-2"
@change="authActive = !authActive" @change="authActive = !authActive"
> >
{{ $t("state.enabled") }} {{ t("state.enabled") }}
</SmartCheckbox> </SmartCheckbox>
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
to="https://docs.hoppscotch.io/features/authorization" to="https://docs.hoppscotch.io/features/authorization"
blank blank
:title="$t('app.wiki')" :title="t('app.wiki')"
svg="help-circle" svg="help-circle"
/> />
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear')" :title="t('action.clear')"
svg="trash-2" svg="trash-2"
@click.native="clearContent" @click.native="clearContent"
/> />
@@ -158,14 +185,14 @@
:src="`/images/states/${$colorMode.value}/login.svg`" :src="`/images/states/${$colorMode.value}/login.svg`"
loading="lazy" loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4" class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="$t('empty.authorization')" :alt="`${t('empty.authorization')}`"
/> />
<span class="pb-4 text-center"> <span class="pb-4 text-center">
This SocketIO connection does not use any authentication. {{ t("socketio.connection_not_authorized") }}
</span> </span>
<ButtonSecondary <ButtonSecondary
outline outline
:label="$t('app.documentation')" :label="t('app.documentation')"
to="https://docs.hoppscotch.io/features/authorization" to="https://docs.hoppscotch.io/features/authorization"
blank blank
svg="external-link" svg="external-link"
@@ -187,343 +214,222 @@
> >
<div class="p-2"> <div class="p-2">
<div class="pb-2 text-secondaryLight"> <div class="pb-2 text-secondaryLight">
{{ $t("helpers.authorization") }} {{ t("helpers.authorization") }}
</div> </div>
<SmartAnchor <SmartAnchor
class="link" class="link"
:label="`${$t('authorization.learn')} \xA0 →`" :label="`${t('authorization.learn')} \xA0 →`"
to="https://docs.hoppscotch.io/features/authorization" to="https://docs.hoppscotch.io/features/authorization"
blank blank
/> />
</div> </div>
</div> </div>
</div> </div>
</SmartTab>
</SmartTabs>
</template> </template>
<template #secondary> <template #secondary>
<RealtimeLog <RealtimeLog
:title="$t('socketio.log')" :title="t('socketio.log')"
:log="log" :log="log"
@delete="clearLogEntries()" @delete="clearLogEntries()"
/> />
</template> </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> </AppPaneLayout>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api" import { onMounted, onUnmounted, ref, watch } 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"
import debounce from "lodash/debounce" import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { import {
SIOEndpoint$, SIOConnection,
setSIOEndpoint, SIOError,
SIOVersion$, SIOMessage,
setSIOVersion, SOCKET_CLIENTS,
SIOPath$, } from "~/helpers/realtime/SIOConnection"
setSIOPath, import {
SIOConnectionState$, useI18n,
SIOConnectingState$, useNuxt,
setSIOConnectionState, useReadonlyStream,
setSIOConnectingState, useStream,
SIOSocket$, useStreamSubscriber,
setSIOSocket, useToast,
SIOLog$, } from "~/helpers/utils/composables"
setSIOLog, import {
addSIOLogLine, addSIOLogLine,
setSIOEndpoint,
setSIOLog,
setSIOPath,
setSIOVersion,
SIOClientVersion,
SIOEndpoint$,
SIOLog$,
SIOPath$,
SIOVersion$,
} from "~/newstore/SocketIOSession" } from "~/newstore/SocketIOSession"
import { useStream } from "~/helpers/utils/composables"
const socketIoClients = { const t = useI18n()
v4: ClientV4, const toast = useToast()
v3: ClientV3, const nuxt = useNuxt()
v2: ClientV2, 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({ const getMessagePayload = (data: SIOMessage): string =>
setup() { typeof data.value === "object" ? JSON.stringify(data.value) : `${data.value}`
return {
socketIoClients, const getErrorPayload = (error: SIOError): string => {
url: useStream(SIOEndpoint$, "", setSIOEndpoint), switch (error.type) {
clientVersion: useStream(SIOVersion$, "", setSIOVersion), case "CONNECTION":
path: useStream(SIOPath$, "", setSIOPath), return t("state.connection_error").toString()
connectingState: useStream( case "RECONNECT_ERROR":
SIOConnectingState$, return t("state.reconnection_error").toString()
false, default:
setSIOConnectingState return t("state.disconnected_from", { name: url.value }).toString()
),
connectionState: useStream(
SIOConnectionState$,
false,
setSIOConnectionState
),
io: useStream(SIOSocket$, null, setSIOSocket),
log: useStream(SIOLog$, [], setSIOLog),
authTypeOptions: ref(null),
} }
}, }
data() {
return { onMounted(() => {
isUrlValid: true, worker = nuxt.value.$worker.createRejexWorker()
communication: { worker.addEventListener("message", workerResponseHandler)
eventName: "",
inputs: [""], subscribeToStream(socket.event$, (event) => {
}, switch (event?.type) {
authType: "None", case "CONNECTING":
bearerToken: "", log.value = [
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 }), payload: `${t("state.connecting_to", { name: url.value })}`,
source: "info", source: "info",
event: "connecting", color: "var(--accent-color)",
ts: Date.now(), ts: undefined,
}, },
] ]
break
try { case "CONNECTED":
if (!this.path) { log.value = [
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 })
}
// 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 }), payload: `${t("state.connected_to", { name: url.value })}`,
source: "info", source: "info",
event: "connected", color: "var(--accent-color)",
ts: Date.now(), ts: event.time,
}, },
] ]
this.$toast.success(this.$t("state.connected")) toast.success(`${t("state.connected")}`)
}) break
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"))
}
logHoppRequestRunToAnalytics({ case "MESSAGE_SENT":
platform: "socketio",
})
},
disconnect() {
this.io.close()
},
handleError(error) {
this.disconnect()
this.connectingState = false
this.connectionState = false
addSIOLogLine({ addSIOLogLine({
payload: this.$t("error.something_went_wrong"), prefix: `[${event.message.eventName}]`,
source: "info", payload: getMessagePayload(event.message),
event: "error",
ts: Date.now(),
})
if (error !== null)
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)}`,
source: "client", source: "client",
ts: Date.now(), ts: event.time,
}) })
this.communication.inputs = [""] 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
} }
}, })
onSelectVersion(version) {
this.clientVersion = version
this.$refs.versionOptions.tippy().hide()
},
clearLogEntries() {
this.log = []
},
},
}) })
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> </script>

View File

@@ -11,11 +11,13 @@
v-model="server" v-model="server"
type="url" type="url"
autocomplete="off" 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" class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark"
:placeholder="$t('sse.url')" :placeholder="$t('sse.url')"
:disabled="connectionSSEState" :disabled="
@keyup.enter="serverValid ? toggleSSEConnection() : null" connectionState === 'STARTED' || connectionState === 'STARTING'
"
@keyup.enter="isUrlValid ? toggleSSEConnection() : null"
/> />
<label <label
for="event-type" for="event-type"
@@ -28,19 +30,23 @@
v-model="eventType" v-model="eventType"
class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark" class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
spellcheck="false" spellcheck="false"
:disabled="connectionSSEState" :disabled="
@keyup.enter="serverValid ? toggleSSEConnection() : null" connectionState === 'STARTED' || connectionState === 'STARTING'
"
@keyup.enter="isUrlValid ? toggleSSEConnection() : null"
/> />
</div> </div>
<ButtonPrimary <ButtonPrimary
id="start" id="start"
:disabled="!serverValid" :disabled="!isUrlValid"
name="start" name="start"
class="w-32" class="w-32"
:label=" :label="
!connectionSSEState ? $t('action.start') : $t('action.stop') connectionState === 'STOPPED'
? t('action.start')
: t('action.stop')
" "
:loading="connectingState" :loading="connectionState === 'STARTING'"
@click.native="toggleSSEConnection" @click.native="toggleSSEConnection"
/> />
</div> </div>
@@ -56,11 +62,10 @@
</AppPaneLayout> </AppPaneLayout>
</template> </template>
<script> <script setup lang="ts">
import { defineComponent } from "@nuxtjs/composition-api" import { ref, watch, onUnmounted, onMounted } from "@nuxtjs/composition-api"
import "splitpanes/dist/splitpanes.css" import "splitpanes/dist/splitpanes.css"
import debounce from "lodash/debounce" import debounce from "lodash/debounce"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { import {
SSEEndpoint$, SSEEndpoint$,
setSSEEndpoint, setSSEEndpoint,
@@ -68,163 +73,127 @@ import {
setSSEEventType, setSSEEventType,
SSESocket$, SSESocket$,
setSSESocket, setSSESocket,
SSEConnectingState$,
SSEConnectionState$,
setSSEConnectionState,
setSSEConnectingState,
SSELog$, SSELog$,
setSSELog, setSSELog,
addSSELogLine, addSSELogLine,
} from "~/newstore/SSESession" } 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({ const t = useI18n()
setup() { const nuxt = useNuxt()
return { const toast = useToast()
connectionSSEState: useStream( const { subscribeToStream } = useStreamSubscriber()
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 = [
{
payload: this.$t("error.browser_support_sse"),
source: "disconnected",
event: "error",
ts: Date.now(),
},
]
}
logHoppRequestRunToAnalytics({ const sse = useStream(SSESocket$, new SSEConnection(), setSSESocket)
platform: "sse", const connectionState = useReadonlyStream(sse.value.connectionState$, "STOPPED")
}) const server = useStream(SSEEndpoint$, "", setSSEEndpoint)
}, const eventType = useStream(SSEEventType$, "", setSSEEventType)
handleSSEError(error) { const log = useStream(SSELog$, [], setSSELog)
this.stop()
this.connectionSSEState = false const isUrlValid = ref(true)
addSSELogLine({
payload: this.$t("error.something_went_wrong"), let worker: Worker
source: "disconnected",
event: "error", const debouncer = debounce(function () {
ts: Date.now(), worker.postMessage({ type: "sse", url: server.value })
}) }, 1000)
if (error !== null)
addSSELogLine({ watch(server, (url) => {
payload: error, if (url) debouncer()
source: "disconnected",
event: "error",
ts: Date.now(),
})
},
stop() {
this.sse.close()
this.sse.onclose()
},
clearLogEntries() {
this.log = []
},
},
}) })
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: `${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
case "MESSAGE_RECEIVED":
addSSELogLine({
payload: event.message,
source: "server",
ts: event.time,
})
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> </script>

View File

@@ -12,40 +12,59 @@
type="url" type="url"
autocomplete="off" autocomplete="off"
spellcheck="false" spellcheck="false"
:class="{ error: !urlValid }" :class="{ error: !isUrlValid }"
:placeholder="$t('websocket.url')" :placeholder="`${t('websocket.url')}`"
:disabled="connectionState" :disabled="
@keyup.enter="urlValid ? toggleConnection() : null" connectionState === 'CONNECTED' ||
connectionState === 'CONNECTING'
"
@keyup.enter="isUrlValid ? toggleConnection() : null"
/> />
<ButtonPrimary <ButtonPrimary
id="connect" id="connect"
:disabled="!urlValid" :disabled="!isUrlValid"
class="w-32" class="w-32"
name="connect" name="connect"
:label=" :label="
!connectionState ? $t('action.connect') : $t('action.disconnect') connectionState === 'DISCONNECTED'
? t('action.connect')
: t('action.disconnect')
" "
:loading="connectingState" :loading="connectionState === 'CONNECTING'"
@click.native="toggleConnection" @click.native="toggleConnection"
/> />
</div> </div>
</div> </div>
<SmartTabs
v-model="selectedTab"
styles="sticky bg-primary top-upperMobilePrimaryStickyFold sm:top-upperPrimaryStickyFold z-10"
>
<SmartTab
:id="'communication'"
:label="`${$t('websocket.communication')}`"
>
<RealtimeCommunication
:is-connected="connectionState === 'CONNECTED'"
@send-message="sendMessage($event)"
></RealtimeCommunication>
</SmartTab>
<SmartTab :id="'protocols'" :label="`${$t('websocket.protocols')}`">
<div <div
class="sticky z-10 flex items-center justify-between pl-4 border-b bg-primary border-dividerLight top-upperPrimaryStickyFold" 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"> <label class="font-semibold text-secondaryLight">
{{ $t("websocket.protocols") }} {{ t("websocket.protocols") }}
</label> </label>
<div class="flex"> <div class="flex">
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="$t('action.clear_all')" :title="t('action.clear_all')"
svg="trash-2" svg="trash-2"
@click.native="clearContent" @click.native="clearContent"
/> />
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="$t('add.new')" :title="t('add.new')"
svg="plus" svg="plus"
@click.native="addProtocol" @click.native="addProtocol"
/> />
@@ -79,7 +98,7 @@
<input <input
v-model="protocol.value" v-model="protocol.value"
class="flex flex-1 px-4 py-2 bg-transparent" class="flex flex-1 px-4 py-2 bg-transparent"
:placeholder="$t('count.protocol', { count: index + 1 })" :placeholder="`${t('count.protocol', { count: index + 1 })}`"
name="message" name="message"
type="text" type="text"
autocomplete="off" autocomplete="off"
@@ -96,9 +115,9 @@
:title=" :title="
protocol.hasOwnProperty('active') protocol.hasOwnProperty('active')
? protocol.active ? protocol.active
? $t('action.turn_off') ? t('action.turn_off')
: $t('action.turn_on') : t('action.turn_on')
: $t('action.turn_off') : t('action.turn_off')
" "
:svg=" :svg="
protocol.hasOwnProperty('active') protocol.hasOwnProperty('active')
@@ -119,10 +138,10 @@
<span> <span>
<ButtonSecondary <ButtonSecondary
v-tippy="{ theme: 'tooltip' }" v-tippy="{ theme: 'tooltip' }"
:title="$t('action.remove')" :title="t('action.remove')"
svg="trash" svg="trash"
color="red" color="red"
@click.native="deleteProtocol({ index })" @click.native="deleteProtocol(index)"
/> />
</span> </span>
</div> </div>
@@ -135,10 +154,14 @@
:src="`/images/states/${$colorMode.value}/add_category.svg`" :src="`/images/states/${$colorMode.value}/add_category.svg`"
loading="lazy" loading="lazy"
class="inline-flex flex-col object-contain object-center w-16 h-16 my-4" class="inline-flex flex-col object-contain object-center w-16 h-16 my-4"
:alt="$t('empty.protocols')" :alt="`${t('empty.protocols')}`"
/> />
<span class="mb-4 text-center">{{ $t("empty.protocols") }}</span> <span class="mb-4 text-center">
{{ t("empty.protocols") }}
</span>
</div> </div>
</SmartTab>
</SmartTabs>
</template> </template>
<template #secondary> <template #secondary>
<RealtimeLog <RealtimeLog
@@ -147,45 +170,12 @@
@delete="clearLogEntries()" @delete="clearLogEntries()"
/> />
</template> </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> </AppPaneLayout>
</template> </template>
<script setup lang="ts">
<script> import { ref, watch, onUnmounted, onMounted } from "@nuxtjs/composition-api"
import { defineComponent } from "@nuxtjs/composition-api"
import debounce from "lodash/debounce" import debounce from "lodash/debounce"
import draggable from "vuedraggable" import draggable from "vuedraggable"
import { logHoppRequestRunToAnalytics } from "~/helpers/fb/analytics"
import { import {
setWSEndpoint, setWSEndpoint,
WSEndpoint$, WSEndpoint$,
@@ -195,62 +185,51 @@ import {
deleteWSProtocol, deleteWSProtocol,
updateWSProtocol, updateWSProtocol,
deleteAllWSProtocols, deleteAllWSProtocols,
WSSocket$,
setWSSocket,
setWSConnectionState,
setWSConnectingState,
WSConnectionState$,
WSConnectingState$,
addWSLogLine, addWSLogLine,
WSLog$, WSLog$,
setWSLog, setWSLog,
HoppWSProtocol,
setWSSocket,
WSSocket$,
} from "~/newstore/WebSocketSession" } 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({ const nuxt = useNuxt()
components: { const t = useI18n()
draggable, const toast = useToast()
}, const { subscribeToStream } = useStreamSubscriber()
setup() {
return { const selectedTab = ref<"communication" | "protocols">("communication")
url: useStream(WSEndpoint$, "", setWSEndpoint), const url = useStream(WSEndpoint$, "", setWSEndpoint)
protocols: useStream(WSProtocols$, [], setWSProtocols), const protocols = useStream(WSProtocols$, [], setWSProtocols)
connectionState: useStream(
WSConnectionState$, const socket = useStream(WSSocket$, new WSConnection(), setWSSocket)
false,
setWSConnectionState const connectionState = useReadonlyStream(
), socket.value.connectionState$,
connectingState: useStream( "DISCONNECTED"
WSConnectingState$, )
false,
setWSConnectingState const log = useStream(WSLog$, [], setWSLog)
), // DATA
socket: useStream(WSSocket$, null, setWSSocket), const isUrlValid = ref(true)
log: useStream(WSLog$, [], setWSLog), const activeProtocols = ref<string[]>([])
} let worker: Worker
}, watch(url, (newUrl) => {
data() { if (newUrl) debouncer()
return { })
isUrlValid: true, watch(
communication: { protocols,
input: "", (newProtocols) => {
}, activeProtocols.value = newProtocols
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) => .filter((item) =>
Object.prototype.hasOwnProperty.call(item, "active") Object.prototype.hasOwnProperty.call(item, "active")
? item.active === true ? item.active === true
@@ -258,181 +237,133 @@ export default defineComponent({
) )
.map(({ value }) => value) .map(({ value }) => value)
}, },
deep: true, { deep: true }
}, )
}, const workerResponseHandler = ({
created() { data,
if (process.browser) { }: {
this.worker = this.$worker.createRejexWorker() data: { url: string; result: boolean }
this.worker.addEventListener("message", this.workerResponseHandler) }) => {
} if (data.url === url.value) isUrlValid.value = data.result
}, }
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"))
}
logHoppRequestRunToAnalytics({ const getErrorPayload = (error: WSErrorMessage): string => {
platform: "wss", if (error instanceof SyntaxError) {
}) return error.message
},
disconnect() {
if (this.socket) {
this.socket.close()
this.connectionState = false
this.connectingState = false
} }
}, return t("error.something_went_wrong").toString()
handleError(error) { }
this.disconnect()
this.connectionState = false onMounted(() => {
addWSLogLine({ worker = nuxt.value.$worker.createRejexWorker()
payload: this.$t("error.something_went_wrong"), 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", source: "info",
event: "error", color: "var(--accent-color)",
ts: Date.now(), ts: undefined,
})
if (error !== null)
addWSLogLine({
payload: error,
source: "info",
event: "error",
ts: Date.now(),
})
}, },
sendMessage() { ]
const message = this.communication.input break
this.socket.send(message)
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({ addWSLogLine({
payload: message, payload: event.message,
source: "client", source: "client",
ts: Date.now(), 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 break
case "down":
if (length > 0 && this.currentIndex > -1) { case "MESSAGE_RECEIVED":
if (this.currentIndex === length - 1) { addWSLogLine({
this.currentIndex = -1 payload: event.message,
this.communication.input = "" source: "server",
} else if (this.currentIndex < length - 1) { ts: event.time,
this.currentIndex = this.currentIndex + 1 })
this.communication.input = break
clientMessages[this.currentIndex].payload
} 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 break
} }
}, })
addProtocol() { })
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 }) addWSProtocol({ value: "", active: true })
}, }
deleteProtocol({ index }) { const deleteProtocol = (index: number) => {
const oldProtocols = this.protocols.slice() const oldProtocols = protocols.value.slice()
deleteWSProtocol(index) deleteWSProtocol(index)
this.$toast.success(this.$t("state.deleted"), { toast.success(`${t("state.deleted")}`, {
action: {
text: this.$t("action.undo"),
duration: 4000, duration: 4000,
action: {
text: `${t("action.undo")}`,
onClick: (_, toastObject) => { onClick: (_, toastObject) => {
this.protocols = oldProtocols protocols.value = oldProtocols
toastObject.remove() toastObject.goAway()
}, },
}, },
}) })
}, }
updateProtocol(index, updated) { const updateProtocol = (index: number, updated: HoppWSProtocol) => {
updateWSProtocol(index, updated) updateWSProtocol(index, updated)
}, }
}, const clearLogEntries = () => {
}) log.value = []
}
</script> </script>

View File

@@ -28,8 +28,6 @@ import { javascriptLanguage } from "@codemirror/lang-javascript"
import { xmlLanguage } from "@codemirror/lang-xml" import { xmlLanguage } from "@codemirror/lang-xml"
import { jsonLanguage } from "@codemirror/lang-json" import { jsonLanguage } from "@codemirror/lang-json"
import { GQLLanguage } from "@hoppscotch/codemirror-lang-graphql" 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 { StreamLanguage } from "@codemirror/stream-parser"
import { html } from "@codemirror/legacy-modes/mode/xml" import { html } from "@codemirror/legacy-modes/mode/xml"
import { shell } from "@codemirror/legacy-modes/mode/shell" 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) => { return linter(async (view) => {
if (!hoppLinter) return []
// Requires full document scan, hence expensive on big files, force disable on big files ? // Requires full document scan, hence expensive on big files, force disable on big files ?
const linterResult = await hoppLinter( const linterResult = await hoppLinter(
view.state.doc.toJSON().join(view.state.lineBreak) view.state.doc.toJSON().join(view.state.lineBreak)
@@ -119,16 +119,16 @@ const hoppLinterExt = (hoppLinter: LinterDefinition): Extension => {
} }
const hoppLang = ( const hoppLang = (
language: Language, language: Language | undefined,
linter?: LinterDefinition | undefined, linter?: LinterDefinition | undefined,
completer?: Completer | undefined completer?: Completer | undefined
) => { ): Extension | LanguageSupport => {
const exts: Extension[] = [] const exts: Extension[] = []
if (linter) exts.push(hoppLinterExt(linter)) exts.push(hoppLinterExt(linter))
if (completer) exts.push(hoppCompleterExt(completer)) if (completer) exts.push(hoppCompleterExt(completer))
return new LanguageSupport(language, exts) return language ? new LanguageSupport(language, exts) : exts
} }
const getLanguage = (langMime: string): Language | null => { const getLanguage = (langMime: string): Language | null => {
@@ -156,12 +156,7 @@ const getEditorLanguage = (
langMime: string, langMime: string,
linter: LinterDefinition | undefined, linter: LinterDefinition | undefined,
completer: Completer | undefined completer: Completer | undefined
): Extension => ): Extension => hoppLang(getLanguage(langMime) ?? undefined, linter, completer)
pipe(
O.fromNullable(getLanguage(langMime)),
O.map((lang) => hoppLang(lang, linter, completer)),
O.getOrElseW(() => [])
)
export function useCodemirror( export function useCodemirror(
el: Ref<any | null>, 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 = { export type HoppRealtimeLogLine = {
prefix?: string
payload: string payload: string
source: string source: string
color?: string color?: string
ts: string ts: number | undefined
} }
export type HoppRealtimeLog = HoppRealtimeLogLine[] export type HoppRealtimeLog = HoppRealtimeLogLine[]

View File

@@ -508,7 +508,8 @@
"event_name": "Event Name", "event_name": "Event Name",
"events": "Events", "events": "Events",
"log": "Log", "log": "Log",
"url": "URL" "url": "URL",
"connection_not_authorized": "This SocketIO connection does not use any authentication."
}, },
"sse": { "sse": {
"event_type": "Event type", "event_type": "Event type",
@@ -538,7 +539,19 @@
"loading": "Loading...", "loading": "Loading...",
"none": "None", "none": "None",
"nothing_found": "Nothing found for", "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": { "support": {
"changelog": "Read more about latest releases", "changelog": "Read more about latest releases",

View File

@@ -1,6 +1,6 @@
import { pluck, distinctUntilChanged } from "rxjs/operators" import { distinctUntilChanged, pluck } from "rxjs/operators"
import { Client as MQTTClient } from "paho-mqtt"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore" import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import { MQTTConnection } from "~/helpers/realtime/MQTTConnection"
import { import {
HoppRealtimeLog, HoppRealtimeLog,
HoppRealtimeLogLine, HoppRealtimeLogLine,
@@ -12,11 +12,9 @@ type HoppMQTTRequest = {
type HoppMQTTSession = { type HoppMQTTSession = {
request: HoppMQTTRequest request: HoppMQTTRequest
connectingState: boolean
connectionState: boolean
subscriptionState: boolean subscriptionState: boolean
log: HoppRealtimeLog log: HoppRealtimeLog
socket: MQTTClient | null socket: MQTTConnection
} }
const defaultMQTTRequest: HoppMQTTRequest = { const defaultMQTTRequest: HoppMQTTRequest = {
@@ -25,10 +23,8 @@ const defaultMQTTRequest: HoppMQTTRequest = {
const defaultMQTTSession: HoppMQTTSession = { const defaultMQTTSession: HoppMQTTSession = {
request: defaultMQTTRequest, request: defaultMQTTRequest,
connectionState: false,
connectingState: false,
subscriptionState: false, subscriptionState: false,
socket: null, socket: new MQTTConnection(),
log: [], log: [],
} }
@@ -48,21 +44,11 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
setSocket(_: HoppMQTTSession, { socket }: { socket: MQTTClient }) { setConn(_: HoppMQTTSession, { socket }: { socket: MQTTConnection }) {
return { return {
socket, socket,
} }
}, },
setConnectionState(_: HoppMQTTSession, { state }: { state: boolean }) {
return {
connectionState: state,
}
},
setConnectingState(_: HoppMQTTSession, { state }: { state: boolean }) {
return {
connectingState: state,
}
},
setSubscriptionState(_: HoppMQTTSession, { state }: { state: boolean }) { setSubscriptionState(_: HoppMQTTSession, { state }: { state: boolean }) {
return { return {
subscriptionState: state, subscriptionState: state,
@@ -100,33 +86,15 @@ export function setMQTTEndpoint(newEndpoint: string) {
}) })
} }
export function setMQTTSocket(socket: MQTTClient) { export function setMQTTConn(socket: MQTTConnection) {
MQTTSessionStore.dispatch({ MQTTSessionStore.dispatch({
dispatcher: "setSocket", dispatcher: "setConn",
payload: { payload: {
socket, 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) { export function setMQTTSubscriptionState(state: boolean) {
MQTTSessionStore.dispatch({ MQTTSessionStore.dispatch({
dispatcher: "setSubscriptionState", dispatcher: "setSubscriptionState",
@@ -179,7 +147,7 @@ export const MQTTSubscriptionState$ = MQTTSessionStore.subject$.pipe(
distinctUntilChanged() distinctUntilChanged()
) )
export const MQTTSocket$ = MQTTSessionStore.subject$.pipe( export const MQTTConn$ = MQTTSessionStore.subject$.pipe(
pluck("socket"), pluck("socket"),
distinctUntilChanged() distinctUntilChanged()
) )

View File

@@ -4,6 +4,7 @@ import {
HoppRealtimeLog, HoppRealtimeLog,
HoppRealtimeLogLine, HoppRealtimeLogLine,
} from "~/helpers/types/HoppRealtimeLog" } from "~/helpers/types/HoppRealtimeLog"
import { SSEConnection } from "~/helpers/realtime/SSEConnection"
type HoppSSERequest = { type HoppSSERequest = {
endpoint: string endpoint: string
@@ -12,10 +13,8 @@ type HoppSSERequest = {
type HoppSSESession = { type HoppSSESession = {
request: HoppSSERequest request: HoppSSERequest
connectingState: boolean
connectionState: boolean
log: HoppRealtimeLog log: HoppRealtimeLog
socket: EventSource | null socket: SSEConnection
} }
const defaultSSERequest: HoppSSERequest = { const defaultSSERequest: HoppSSERequest = {
@@ -25,9 +24,7 @@ const defaultSSERequest: HoppSSERequest = {
const defaultSSESession: HoppSSESession = { const defaultSSESession: HoppSSESession = {
request: defaultSSERequest, request: defaultSSERequest,
connectionState: false, socket: new SSEConnection(),
connectingState: false,
socket: null,
log: [], log: [],
} }
@@ -56,21 +53,11 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
setSocket(_: HoppSSESession, { socket }: { socket: EventSource }) { setSocket(_: HoppSSESession, { socket }: { socket: SSEConnection }) {
return { return {
socket, socket,
} }
}, },
setConnectionState(_: HoppSSESession, { state }: { state: boolean }) {
return {
connectionState: state,
}
},
setConnectingState(_: HoppSSESession, { state }: { state: boolean }) {
return {
connectingState: state,
}
},
setLog(_: HoppSSESession, { log }: { log: HoppRealtimeLog }) { setLog(_: HoppSSESession, { log }: { log: HoppRealtimeLog }) {
return { return {
log, log,
@@ -112,7 +99,7 @@ export function setSSEEventType(newType: string) {
}) })
} }
export function setSSESocket(socket: EventSource) { export function setSSESocket(socket: SSEConnection) {
SSESessionStore.dispatch({ SSESessionStore.dispatch({
dispatcher: "setSocket", dispatcher: "setSocket",
payload: { 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) { export function setSSELog(log: HoppRealtimeLog) {
SSESessionStore.dispatch({ SSESessionStore.dispatch({
dispatcher: "setLog", dispatcher: "setLog",
@@ -176,11 +146,6 @@ export const SSEConnectingState$ = SSESessionStore.subject$.pipe(
distinctUntilChanged() distinctUntilChanged()
) )
export const SSEConnectionState$ = SSESessionStore.subject$.pipe(
pluck("connectionState"),
distinctUntilChanged()
)
export const SSESocket$ = SSESessionStore.subject$.pipe( export const SSESocket$ = SSESessionStore.subject$.pipe(
pluck("socket"), pluck("socket"),
distinctUntilChanged() distinctUntilChanged()

View File

@@ -10,16 +10,16 @@ import {
type SocketIO = SocketV2 | SocketV3 | SocketV4 type SocketIO = SocketV2 | SocketV3 | SocketV4
export type SIOClientVersion = "v4" | "v3" | "v2"
type HoppSIORequest = { type HoppSIORequest = {
endpoint: string endpoint: string
path: string path: string
version: string version: SIOClientVersion
} }
type HoppSIOSession = { type HoppSIOSession = {
request: HoppSIORequest request: HoppSIORequest
connectingState: boolean
connectionState: boolean
log: HoppRealtimeLog log: HoppRealtimeLog
socket: SocketIO | null socket: SocketIO | null
} }
@@ -32,8 +32,6 @@ const defaultSIORequest: HoppSIORequest = {
const defaultSIOSession: HoppSIOSession = { const defaultSIOSession: HoppSIOSession = {
request: defaultSIORequest, request: defaultSIORequest,
connectionState: false,
connectingState: false,
socket: null, socket: null,
log: [], log: [],
} }
@@ -63,7 +61,10 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
setVersion(curr: HoppSIOSession, { newVersion }: { newVersion: string }) { setVersion(
curr: HoppSIOSession,
{ newVersion }: { newVersion: SIOClientVersion }
) {
return { return {
request: { request: {
...curr.request, ...curr.request,
@@ -76,16 +77,6 @@ const dispatchers = defineDispatchers({
socket, socket,
} }
}, },
setConnectionState(_: HoppSIOSession, { state }: { state: boolean }) {
return {
connectionState: state,
}
},
setConnectingState(_: HoppSIOSession, { state }: { state: boolean }) {
return {
connectingState: state,
}
},
setLog(_: HoppSIOSession, { log }: { log: HoppRealtimeLog }) { setLog(_: HoppSIOSession, { log }: { log: HoppRealtimeLog }) {
return { return {
log, 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) { export function setSIOLog(log: HoppRealtimeLog) {
SIOSessionStore.dispatch({ SIOSessionStore.dispatch({
dispatcher: "setLog", dispatcher: "setLog",
@@ -200,11 +174,6 @@ export const SIOPath$ = SIOSessionStore.subject$.pipe(
distinctUntilChanged() distinctUntilChanged()
) )
export const SIOConnectingState$ = SIOSessionStore.subject$.pipe(
pluck("connectingState"),
distinctUntilChanged()
)
export const SIOConnectionState$ = SIOSessionStore.subject$.pipe( export const SIOConnectionState$ = SIOSessionStore.subject$.pipe(
pluck("connectionState"), pluck("connectionState"),
distinctUntilChanged() distinctUntilChanged()

View File

@@ -4,8 +4,9 @@ import {
HoppRealtimeLog, HoppRealtimeLog,
HoppRealtimeLogLine, HoppRealtimeLogLine,
} from "~/helpers/types/HoppRealtimeLog" } from "~/helpers/types/HoppRealtimeLog"
import { WSConnection } from "~/helpers/realtime/WSConnection"
type HoppWSProtocol = { export type HoppWSProtocol = {
value: string value: string
active: boolean active: boolean
} }
@@ -17,10 +18,8 @@ type HoppWSRequest = {
export type HoppWSSession = { export type HoppWSSession = {
request: HoppWSRequest request: HoppWSRequest
connectingState: boolean
connectionState: boolean
log: HoppRealtimeLog log: HoppRealtimeLog
socket: WebSocket | null socket: WSConnection
} }
const defaultWSRequest: HoppWSRequest = { const defaultWSRequest: HoppWSRequest = {
@@ -30,9 +29,7 @@ const defaultWSRequest: HoppWSRequest = {
const defaultWSSession: HoppWSSession = { const defaultWSSession: HoppWSSession = {
request: defaultWSRequest, request: defaultWSRequest,
connectionState: false, socket: new WSConnection(),
connectingState: false,
socket: null,
log: [], log: [],
} }
@@ -101,21 +98,11 @@ const dispatchers = defineDispatchers({
}, },
} }
}, },
setSocket(_: HoppWSSession, { socket }: { socket: WebSocket }) { setSocket(_: HoppWSSession, { socket }: { socket: WSConnection }) {
return { return {
socket, socket,
} }
}, },
setConnectionState(_: HoppWSSession, { state }: { state: boolean }) {
return {
connectionState: state,
}
},
setConnectingState(_: HoppWSSession, { state }: { state: boolean }) {
return {
connectingState: state,
}
},
setLog(_: HoppWSSession, { log }: { log: HoppRealtimeLog }) { setLog(_: HoppWSSession, { log }: { log: HoppRealtimeLog }) {
return { return {
log, log,
@@ -195,7 +182,7 @@ export function updateWSProtocol(
}) })
} }
export function setWSSocket(socket: WebSocket) { export function setWSSocket(socket: WSConnection) {
WSSessionStore.dispatch({ WSSessionStore.dispatch({
dispatcher: "setSocket", dispatcher: "setSocket",
payload: { 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) { export function setWSLog(log: HoppRealtimeLog) {
WSSessionStore.dispatch({ WSSessionStore.dispatch({
dispatcher: "setLog", dispatcher: "setLog",

View File

@@ -153,6 +153,7 @@
"@types/paho-mqtt": "^1.0.6", "@types/paho-mqtt": "^1.0.6",
"@types/postman-collection": "^3.5.7", "@types/postman-collection": "^3.5.7",
"@types/qs": "^6.9.7", "@types/qs": "^6.9.7",
"@types/socketio-wildcard": "^2.0.4",
"@types/splitpanes": "^2.2.1", "@types/splitpanes": "^2.2.1",
"@types/uuid": "^8.3.4", "@types/uuid": "^8.3.4",
"@types/yargs-parser": "^21.0.0", "@types/yargs-parser": "^21.0.0",

View File

@@ -2,6 +2,21 @@
// tsc, this is really annoying (and maybe dangerous) // 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, // 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 // 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