github.com/tilt-dev/tilt@v0.33.15-0.20240515162809-0a22ed45d8a0/web/src/AppController.ts (about)

     1  import HudState from "./HudState"
     2  import PathBuilder from "./PathBuilder"
     3  import { Snapshot, SocketState } from "./types"
     4  
     5  interface HudInt {
     6    onAppChange: <K extends keyof HudState>(state: Pick<HudState, K>) => void
     7    setHistoryLocation: (path: string) => void
     8  }
     9  
    10  // A Websocket that automatically retries.
    11  class AppController {
    12    url: string
    13    loadCount: number
    14    liveSocket: boolean
    15    tryConnectCount: number
    16    socket: WebSocket | null = null
    17    component: HudInt
    18    disposed: boolean = false
    19    pb: PathBuilder
    20  
    21    /**
    22     * @param pathBuilder a PathBuilder
    23     * @param component The top-level component for the app.
    24     *     Has one method, onAppChange, that receives all the updates
    25     *     for the application.
    26     */
    27    constructor(pathBuilder: PathBuilder, component: HudInt) {
    28      if (!component.onAppChange) {
    29        throw new Error("App component has no onAppChange method")
    30      }
    31  
    32      this.pb = pathBuilder
    33      this.url = pathBuilder.getDataUrl()
    34      this.component = component
    35      this.tryConnectCount = 0
    36      this.liveSocket = false
    37      this.loadCount = 0
    38    }
    39  
    40    createNewSocket() {
    41      this.tryConnectCount++
    42      fetch("/api/websocket_token")
    43        .then((res) => res.text())
    44        .then((text) => {
    45          this.socket = new WebSocket(`${this.url}?csrf=${text}`)
    46  
    47          this.socket.addEventListener("close", this.onSocketClose.bind(this))
    48          this.socket.addEventListener("message", (event) => {
    49            if (!this.liveSocket) {
    50              this.loadCount++
    51            }
    52            this.liveSocket = true
    53            this.tryConnectCount = 0
    54  
    55            let data: Proto.webviewView = JSON.parse(event.data)
    56  
    57            // @ts-ignore
    58            this.component.onAppChange({
    59              view: data,
    60              socketState: SocketState.Active,
    61            })
    62          })
    63        })
    64        .catch((err) => {
    65          console.error("fetching websocket token: " + err)
    66          this.onSocketClose()
    67        })
    68    }
    69  
    70    dispose() {
    71      this.disposed = true
    72      if (this.socket) {
    73        this.socket.close()
    74      }
    75    }
    76  
    77    onSocketClose() {
    78      let wasAlive = this.liveSocket
    79      this.liveSocket = false
    80      if (this.disposed) {
    81        return
    82      }
    83  
    84      if (wasAlive) {
    85        this.component.onAppChange({
    86          socketState: SocketState.Closed,
    87        })
    88        this.createNewSocket()
    89        return
    90      }
    91  
    92      let backoff = Math.pow(2, this.tryConnectCount) * 1000
    93      let maxTimeout = 10 * 1000 // 10sec
    94      let isLocal =
    95        this.url.indexOf("ws://localhost") === 0 ||
    96        this.url.indexOf("wss://localhost") === 0
    97      if (isLocal) {
    98        // if this is a local connection, max out at 1.5sec.
    99        // this makes it a bit easier to detect when a window is already open.
   100        maxTimeout = 1500
   101      }
   102      let timeout = Math.min(maxTimeout, backoff)
   103  
   104      setTimeout(() => {
   105        if (this.disposed) {
   106          return
   107        }
   108        let state: SocketState =
   109          this.loadCount || this.tryConnectCount > 1
   110            ? SocketState.Reconnecting
   111            : SocketState.Loading
   112        this.component.onAppChange({
   113          socketState: state,
   114        })
   115        this.createNewSocket()
   116      }, timeout)
   117    }
   118  
   119    setStateFromSnapshot(): void {
   120      let url = this.url
   121      fetch(url)
   122        .then((resp) => resp.json())
   123        .then((data: Snapshot) => {
   124          data.view = data.view || {}
   125  
   126          this.component.onAppChange({
   127            view: data.view,
   128            snapshotStartTime: data.createdAt,
   129          })
   130  
   131          if (data.path) {
   132            this.component.setHistoryLocation(this.pb.path(data.path))
   133          }
   134        })
   135        .catch((err) => {
   136          console.error(err)
   137          this.component.onAppChange({ error: err })
   138        })
   139    }
   140  }
   141  
   142  export default AppController