Эх сурвалжийг харах

reactor:智能回访和明细新增导出;智能外呼任务和短信任务事件选择至少今天;

zhangchong 10 сар өмнө
parent
commit
ed6a65ae67
1 өөрчлөгдсөн 319 нэмэгдсэн , 0 устгасан
  1. 319 0
      src/hooks/useWebsocket.ts

+ 319 - 0
src/hooks/useWebsocket.ts

@@ -0,0 +1,319 @@
+import type { Ref } from 'vue'
+import { ref, watch } from 'vue'
+import type { Fn, MaybeRefOrGetter } from '@vueuse/shared'
+import { isClient, isWorker, toRef, tryOnScopeDispose, useIntervalFn } from '@vueuse/shared'
+
+export type WebSocketStatus = 'OPEN' | 'CONNECTING' | 'CLOSED'
+
+const DEFAULT_PING_MESSAGE = 'ping'
+
+export interface UseWebSocketOptions {
+  onConnected?: (ws: WebSocket) => void
+  onDisconnected?: (ws: WebSocket, event: CloseEvent) => void
+  onError?: (ws: WebSocket, event: Event) => void
+  onMessage?: (ws: WebSocket, event: MessageEvent) => void
+
+  /**
+   * Send heartbeat for every x milliseconds passed
+   *
+   * @default false
+   */
+  heartbeat?: boolean | {
+    /**
+     * Message for the heartbeat
+     *
+     * @default 'ping'
+     */
+    message?: string | ArrayBuffer | Blob
+
+    /**
+     * Interval, in milliseconds
+     *
+     * @default 1000
+     */
+    interval?: number
+
+    /**
+     * Heartbeat response timeout, in milliseconds
+     *
+     * @default 1000
+     */
+    pongTimeout?: number
+  }
+
+  /**
+   * Enabled auto reconnect
+   *
+   * @default false
+   */
+  autoReconnect?: boolean | {
+    /**
+     * Maximum retry times.
+     *
+     * Or you can pass a predicate function (which returns true if you want to retry).
+     *
+     * @default -1
+     */
+    retries?: number | (() => boolean)
+
+    /**
+     * Delay for reconnect, in milliseconds
+     *
+     * @default 1000
+     */
+    delay?: number
+
+    /**
+     * On maximum retry times reached.
+     */
+    onFailed?: Fn
+  }
+
+  /**
+   * Automatically open a connection
+   *
+   * @default true
+   */
+  immediate?: boolean
+
+  /**
+   * Automatically close a connection
+   *
+   * @default true
+   */
+  autoClose?: boolean
+
+  /**
+   * List of one or more sub-protocol strings
+   *
+   * @default []
+   */
+  protocols?: string[]
+}
+
+export interface UseWebSocketReturn<T> {
+  /**
+   * Reference to the latest data received via the websocket,
+   * can be watched to respond to incoming messages
+   */
+  data: Ref<T | null>
+
+  /**
+   * The current websocket status, can be only one of:
+   * 'OPEN', 'CONNECTING', 'CLOSED'
+   */
+  status: Ref<WebSocketStatus>
+
+  /**
+   * Closes the websocket connection gracefully.
+   */
+  close: WebSocket['close']
+
+  /**
+   * Reopen the websocket connection.
+   * If there the current one is active, will close it before opening a new one.
+   */
+  open: Fn
+
+  /**
+   * Sends data through the websocket connection.
+   *
+   * @param data
+   * @param useBuffer when the socket is not yet open, store the data into the buffer and sent them one connected. Default to true.
+   */
+  send: (data: string | ArrayBuffer | Blob, useBuffer?: boolean) => boolean
+
+  /**
+   * Reference to the WebSocket instance.
+   */
+  ws: Ref<WebSocket | undefined>
+}
+
+function resolveNestedOptions<T>(options: T | true): T {
+  if (options === true)
+    return {} as T
+  return options
+}
+
+/**
+ * Reactive WebSocket client.
+ *
+ * @see https://vueuse.org/useWebSocket
+ * @param url
+ * @param options
+ */
+export function useWebSocket<Data = any>(
+  url: MaybeRefOrGetter<string | URL | undefined>,
+  options: UseWebSocketOptions = {},
+): UseWebSocketReturn<Data> {
+  const {
+    onConnected,
+    onDisconnected,
+    onError,
+    onMessage,
+    immediate = true,
+    autoClose = true,
+    protocols = [],
+  } = options
+
+  const data: Ref<Data | null> = ref(null)
+  const status = ref<WebSocketStatus>('CLOSED')
+  const wsRef = ref<WebSocket | undefined>()
+  const urlRef = toRef(url)
+
+  let heartbeatPause: Fn | undefined
+  let heartbeatResume: Fn | undefined
+
+  let explicitlyClosed = false
+  let retried = 0
+
+  let bufferedData: (string | ArrayBuffer | Blob)[] = []
+
+  let pongTimeoutWait: ReturnType<typeof setTimeout> | undefined
+
+  const _sendBuffer = () => {
+    if (bufferedData.length && wsRef.value && status.value === 'OPEN') {
+      for (const buffer of bufferedData)
+        wsRef.value.send(buffer)
+      bufferedData = []
+    }
+  }
+
+  const resetHeartbeat = () => {
+    clearTimeout(pongTimeoutWait)
+    pongTimeoutWait = undefined
+  }
+
+  // Status code 1000 -> Normal Closure https://developer.mozilla.org/en-US/docs/Web/API/CloseEvent/code
+  const close: WebSocket['close'] = (code = 1000, reason) => {
+    if (!isClient || !wsRef.value)
+      return
+    explicitlyClosed = true
+    resetHeartbeat()
+    heartbeatPause?.()
+    wsRef.value.close(code, reason)
+    wsRef.value = undefined
+  }
+
+  const send = (data: string | ArrayBuffer | Blob, useBuffer = true) => {
+    if (!wsRef.value || status.value !== 'OPEN') {
+      if (useBuffer)
+        bufferedData.push(data)
+      return false
+    }
+    _sendBuffer()
+    wsRef.value.send(data)
+    return true
+  }
+
+  const _init = () => {
+    if (explicitlyClosed || typeof urlRef.value === 'undefined')
+      return
+
+    const ws = new WebSocket(urlRef.value, protocols)
+    wsRef.value = ws
+    status.value = 'CONNECTING'
+
+    ws.onopen = () => {
+      status.value = 'OPEN';
+      onConnected?.(ws!)
+      heartbeatResume?.()
+      _sendBuffer()
+    }
+
+    ws.onclose = (ev) => {
+      status.value = 'CLOSED'
+      onDisconnected?.(ws, ev)
+
+      if (!explicitlyClosed && options.autoReconnect) {
+        const {
+          retries = -1,
+          delay = 1000,
+          onFailed,
+        } = resolveNestedOptions(options.autoReconnect)
+        retried += 1
+
+        // @ts-ignore
+        if ((retries < 0 || retried < retries))
+          setTimeout(_init, delay)
+        else if (typeof retries === 'function' && retries())
+          setTimeout(_init, delay)
+        else
+          onFailed?.()
+      }
+    }
+
+    ws.onerror = (e) => {
+      onError?.(ws!, e)
+    }
+
+    ws.onmessage = (e: MessageEvent) => {
+      if (options.heartbeat) {
+        resetHeartbeat()
+        const {
+          message = DEFAULT_PING_MESSAGE,
+        } = resolveNestedOptions(options.heartbeat)
+        if (e.data === message)
+          return
+      }
+
+      data.value = e.data
+      onMessage?.(ws!, e)
+    }
+  }
+
+  if (options.heartbeat) {
+    const {
+      message = DEFAULT_PING_MESSAGE,
+      interval = 1000,
+      pongTimeout = 1000,
+    } = resolveNestedOptions(options.heartbeat)
+
+    const { pause, resume } = useIntervalFn(
+      () => {
+        send(message, false)
+        if (pongTimeoutWait != null)
+          return
+        pongTimeoutWait = setTimeout(() => {
+          // auto-reconnect will be trigger with ws.onclose()
+          close()
+          explicitlyClosed = false
+        }, pongTimeout)
+      },
+      interval,
+      { immediate: false },
+    )
+
+    heartbeatPause = pause
+    heartbeatResume = resume
+  }
+
+  if (autoClose) {
+    if (isClient)
+      window.addEventListener('beforeunload', () => close())
+    tryOnScopeDispose(close)
+  }
+
+  const open = () => {
+    if (!isClient && !isWorker)
+      return
+    close()
+    explicitlyClosed = false
+    retried = 0
+    _init()
+  }
+
+  if (immediate)
+    open()
+
+  watch(urlRef, open)
+
+  return {
+    data,
+    status,
+    close,
+    send,
+    open,
+    ws: wsRef,
+  }
+}