sse.vue 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. <template>
  2. <AppPaneLayout layout-id="sse">
  3. <template #primary>
  4. <div
  5. class="sticky top-0 z-10 flex flex-shrink-0 p-4 overflow-x-auto space-x-2 bg-primary hide-scrollbar"
  6. >
  7. <div class="inline-flex flex-1 space-x-2">
  8. <div class="flex flex-1">
  9. <input
  10. id="server"
  11. v-model="server"
  12. type="url"
  13. autocomplete="off"
  14. :class="{ error: !isUrlValid }"
  15. class="flex flex-1 w-full px-4 py-2 border rounded-l bg-primaryLight border-divider text-secondaryDark"
  16. :placeholder="$t('sse.url')"
  17. :disabled="
  18. connectionState === 'STARTED' || connectionState === 'STARTING'
  19. "
  20. @keyup.enter="isUrlValid ? toggleSSEConnection() : null"
  21. />
  22. <label
  23. for="event-type"
  24. class="px-4 py-2 font-semibold truncate border-t border-b bg-primaryLight border-divider text-secondaryLight"
  25. >
  26. {{ $t("sse.event_type") }}
  27. </label>
  28. <input
  29. id="event-type"
  30. v-model="eventType"
  31. class="flex flex-1 w-full px-4 py-2 border rounded-r bg-primaryLight border-divider text-secondaryDark"
  32. spellcheck="false"
  33. :disabled="
  34. connectionState === 'STARTED' || connectionState === 'STARTING'
  35. "
  36. @keyup.enter="isUrlValid ? toggleSSEConnection() : null"
  37. />
  38. </div>
  39. <ButtonPrimary
  40. id="start"
  41. :disabled="!isUrlValid"
  42. name="start"
  43. class="w-32"
  44. :label="
  45. connectionState === 'STOPPED'
  46. ? t('action.start')
  47. : t('action.stop')
  48. "
  49. :loading="connectionState === 'STARTING'"
  50. @click.native="toggleSSEConnection"
  51. />
  52. </div>
  53. </div>
  54. </template>
  55. <template #secondary>
  56. <RealtimeLog
  57. :title="$t('sse.log')"
  58. :log="log"
  59. @delete="clearLogEntries()"
  60. />
  61. </template>
  62. </AppPaneLayout>
  63. </template>
  64. <script setup lang="ts">
  65. import { ref, watch, onUnmounted, onMounted } from "@nuxtjs/composition-api"
  66. import "splitpanes/dist/splitpanes.css"
  67. import debounce from "lodash/debounce"
  68. import {
  69. SSEEndpoint$,
  70. setSSEEndpoint,
  71. SSEEventType$,
  72. setSSEEventType,
  73. SSESocket$,
  74. setSSESocket,
  75. SSELog$,
  76. setSSELog,
  77. addSSELogLine,
  78. } from "~/newstore/SSESession"
  79. import {
  80. useNuxt,
  81. useStream,
  82. useToast,
  83. useI18n,
  84. useStreamSubscriber,
  85. useReadonlyStream,
  86. } from "~/helpers/utils/composables"
  87. import { SSEConnection } from "~/helpers/realtime/SSEConnection"
  88. const t = useI18n()
  89. const nuxt = useNuxt()
  90. const toast = useToast()
  91. const { subscribeToStream } = useStreamSubscriber()
  92. const sse = useStream(SSESocket$, new SSEConnection(), setSSESocket)
  93. const connectionState = useReadonlyStream(sse.value.connectionState$, "STOPPED")
  94. const server = useStream(SSEEndpoint$, "", setSSEEndpoint)
  95. const eventType = useStream(SSEEventType$, "", setSSEEventType)
  96. const log = useStream(SSELog$, [], setSSELog)
  97. const isUrlValid = ref(true)
  98. let worker: Worker
  99. const debouncer = debounce(function () {
  100. worker.postMessage({ type: "sse", url: server.value })
  101. }, 1000)
  102. watch(server, (url) => {
  103. if (url) debouncer()
  104. })
  105. const workerResponseHandler = ({
  106. data,
  107. }: {
  108. data: { url: string; result: boolean }
  109. }) => {
  110. if (data.url === server.value) isUrlValid.value = data.result
  111. }
  112. onMounted(() => {
  113. worker = nuxt.value.$worker.createRejexWorker()
  114. worker.addEventListener("message", workerResponseHandler)
  115. subscribeToStream(sse.value.event$, (event) => {
  116. switch (event?.type) {
  117. case "STARTING":
  118. log.value = [
  119. {
  120. payload: `${t("state.connecting_to", { name: server.value })}`,
  121. source: "info",
  122. color: "var(--accent-color)",
  123. ts: undefined,
  124. },
  125. ]
  126. break
  127. case "STARTED":
  128. log.value = [
  129. {
  130. payload: `${t("state.connected_to", { name: server.value })}`,
  131. source: "info",
  132. color: "var(--accent-color)",
  133. ts: Date.now(),
  134. },
  135. ]
  136. toast.success(`${t("state.connected")}`)
  137. break
  138. case "MESSAGE_RECEIVED":
  139. addSSELogLine({
  140. payload: event.message,
  141. source: "server",
  142. ts: event.time,
  143. })
  144. break
  145. case "ERROR":
  146. addSSELogLine({
  147. payload: t("error.browser_support_sse").toString(),
  148. source: "info",
  149. color: "#ff5555",
  150. ts: event.time,
  151. })
  152. break
  153. case "STOPPED":
  154. addSSELogLine({
  155. payload: t("state.disconnected_from", {
  156. name: server.value,
  157. }).toString(),
  158. source: "info",
  159. color: "#ff5555",
  160. ts: event.time,
  161. })
  162. toast.error(`${t("state.disconnected")}`)
  163. break
  164. }
  165. })
  166. })
  167. // METHODS
  168. const toggleSSEConnection = () => {
  169. // If it is connecting:
  170. if (connectionState.value === "STOPPED") {
  171. return sse.value.start(server.value, eventType.value)
  172. }
  173. // Otherwise, it's disconnecting.
  174. sse.value.stop()
  175. }
  176. onUnmounted(() => {
  177. worker.terminate()
  178. })
  179. const clearLogEntries = () => {
  180. log.value = []
  181. }
  182. </script>