Home Reference Source

js/signaler/LocalSignaler.js

import SignalerBase from './Base.js'
import IntervalTimer from '../util/IntervalTimer.js'
import TimeoutTimer from '../util/TimeoutTimer.js'

/**
 * 한 기기에서 탭끼리 연결하는데 사용할 수 있는 시그널러. `BroadcastChannel`을 이용해 시그널을 주고받습니다.
 */
export default class LocalSignaler extends SignalerBase {
  /**
   * 연결을 시작합니다.
   * @param {object} userConfig
   * @param {number} [userConfig.heartbeatInterval] heartbeat 메시지를 보낼 시간 간격(ms)
   * @param {number} [userConfig.heartbeatTimeout] 마지막으로 heartbeat 메시지를 받은 후 이 시간(ms)동안 heartbeat 메시지를 받지 못하면 연결이 끊긴걸로 간주합니다.
   */
  constructor (userConfig) {
    super()

    // 설정 합치기
    this.config = {
      heartbeatInterval: 1000,
      heartbeatTimeout: 2000
    }
    Object.assign(this.config, userConfig)

    /**
     * 피어를 구분하는 id. 탭을 두개 열어놓고 한쪽을 새로고침시 재연결하는걸 막기 위해서 도입되었습니다.
     */
    this.id = Math.random().toString(36).substring(2)

    /**
     * 연결되어 있는 sender의 id
     */
    this.sender = null

    /**
     * 통신이 이루어질 BroadcastChannel
     */
    this.bc = new BroadcastChannel('broadcast-channel-signaler')

    /**
     * 설정된 주기마다 heartbeat 메시지를 보내는 타이머
     */
    this.sendHeartbeatTimer = new IntervalTimer(() => {
      const heartbeatMsg = JSON.stringify({ type: 'heartbeat', src: this.id })
      this.bc.postMessage(heartbeatMsg)
    }, this.config.heartbeatInterval)

    /**
     * 설정된 시간 동안 heartbeat를 받지 못하면 연결이 꾾어진 것으로 판정하는 타이머
     */
    this.heartbeatTimeoutTimer = new TimeoutTimer(() => {
      this.ready.set(false)
    }, this.config.heartbeatTimeout, { autoStart: false })

    // broadcast channel로부터 메시지를 받는 헨들러
    this.bc.addEventListener('message', evt => {
      const msg = JSON.parse(evt.data)

      // heartbeat 메시지를 받으면 연결된걸로 간주
      if (msg.type === 'heartbeat') {
        this.receiveHeartbeat(msg.src)
        return
      }

      // 디버깅을 위해 incoming-msg 이벤트 발생
      // (heartbeat 메시지는 무시)
      this.emit('incoming-msg', evt.data)

      // 엔진에 메시지 전달
      this.receive(msg)
    })
  }

  /**
   * heartbeat timeout을 취소하고 ready를 false로 설정합니다.
   * @private
   */
  receiveHeartbeat (sender) {
    if (this.sender === null) {
      // 메시지를 처음 받았을 경우
      this.sender = sender
    } else if (sender !== this.sender) {
      // 보낸 사람의 id가 일치하지 않을 경우
      return
    }

    this.ready.set(true)
    this.heartbeatTimeoutTimer.reset()
  }

  /**
   * 상대에게 메시지를 전송합니다.
   * @param {*} msg 전송할 메시지.
   */
  send (msg) {
    const data = JSON.stringify(msg)
    this.emit('outgoing-msg', data)

    // postMessage는 JSON으로 바꾸지 않아도 오브젝트를 보낼 수 있지만
    // RTCSessionDescription을 보내면 오류가 남, 따라서 JSON으로 바꿔줘야 함
    this.bc.postMessage(data)
  }

  /**
   * close 훅. Broadcast Channel을 닫습니다.
   */
  close () {
    this.heartbeatTimeoutTimer.clear()
    this.sendHeartbeatTimer.clear()
    this.bc.close()
  }
}