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