|
| 1 | +import { Api } from "coder/site/src/api/api" |
| 2 | +import { Workspace, GetInboxNotificationResponse } from "coder/site/src/api/typesGenerated" |
| 3 | +import { ProxyAgent } from "proxy-agent" |
| 4 | +import * as vscode from "vscode" |
| 5 | +import { WebSocket } from "ws" |
| 6 | +import { errToStr } from "./api-helper" |
| 7 | +import { type Storage } from "./storage" |
| 8 | + |
| 9 | +// These are the template IDs of our notifications. |
| 10 | +// Maybe in the future we should avoid hardcoding |
| 11 | +// these in both coderd and here. |
| 12 | +const TEMPLATE_WORKSPACE_OUT_OF_MEMORY = "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a" |
| 13 | +const TEMPLATE_WORKSPACE_OUT_OF_DISK = "f047f6a3-5713-40f7-85aa-0394cce9fa3a" |
| 14 | + |
| 15 | +export class Inbox implements vscode.Disposable { |
| 16 | + readonly #storage: Storage |
| 17 | + #disposed = false |
| 18 | + #socket: WebSocket |
| 19 | + |
| 20 | + constructor(workspace: Workspace, httpAgent: ProxyAgent, restClient: Api, storage: Storage) { |
| 21 | + this.#storage = storage |
| 22 | + |
| 23 | + const baseUrlRaw = restClient.getAxiosInstance().defaults.baseURL |
| 24 | + if (!baseUrlRaw) { |
| 25 | + throw new Error("No base URL set on REST client") |
| 26 | + } |
| 27 | + |
| 28 | + const watchTemplates = [TEMPLATE_WORKSPACE_OUT_OF_DISK, TEMPLATE_WORKSPACE_OUT_OF_MEMORY] |
| 29 | + const watchTemplatesParam = encodeURIComponent(watchTemplates.join(",")) |
| 30 | + |
| 31 | + const watchTargets = [workspace.id] |
| 32 | + const watchTargetsParam = encodeURIComponent(watchTargets.join(",")) |
| 33 | + |
| 34 | + // We shouldn't need to worry about this throwing. Whilst `baseURL` could |
| 35 | + // be an invalid URL, that would've caused issues before we got to here. |
| 36 | + const baseUrl = new URL(baseUrlRaw) |
| 37 | + const socketProto = baseUrl.protocol === "https:" ? "wss:" : "ws:" |
| 38 | + const socketUrl = `${socketProto}//${baseUrl.host}/api/v2/notifications/inbox/watch?format=plaintext&templates=${watchTemplatesParam}&targets=${watchTargetsParam}` |
| 39 | + |
| 40 | + const coderSessionTokenHeader = "Coder-Session-Token" |
| 41 | + this.#socket = new WebSocket(new URL(socketUrl), { |
| 42 | + followRedirects: true, |
| 43 | + agent: httpAgent, |
| 44 | + headers: { |
| 45 | + [coderSessionTokenHeader]: restClient.getAxiosInstance().defaults.headers.common[coderSessionTokenHeader] as |
| 46 | + | string |
| 47 | + | undefined, |
| 48 | + }, |
| 49 | + }) |
| 50 | + |
| 51 | + this.#socket.on("open", () => { |
| 52 | + this.#storage.writeToCoderOutputChannel("Listening to Coder Inbox") |
| 53 | + }) |
| 54 | + |
| 55 | + this.#socket.on("error", (error) => { |
| 56 | + this.notifyError(error) |
| 57 | + this.dispose() |
| 58 | + }) |
| 59 | + |
| 60 | + this.#socket.on("message", (data) => { |
| 61 | + try { |
| 62 | + const inboxMessage = JSON.parse(data.toString()) as GetInboxNotificationResponse |
| 63 | + |
| 64 | + vscode.window.showInformationMessage(inboxMessage.notification.title) |
| 65 | + } catch (error) { |
| 66 | + this.notifyError(error) |
| 67 | + } |
| 68 | + }) |
| 69 | + } |
| 70 | + |
| 71 | + dispose() { |
| 72 | + if (!this.#disposed) { |
| 73 | + this.#storage.writeToCoderOutputChannel("No longer listening to Coder Inbox") |
| 74 | + this.#socket.close() |
| 75 | + this.#disposed = true |
| 76 | + } |
| 77 | + } |
| 78 | + |
| 79 | + private notifyError(error: unknown) { |
| 80 | + const message = errToStr(error, "Got empty error while monitoring Coder Inbox") |
| 81 | + this.#storage.writeToCoderOutputChannel(message) |
| 82 | + } |
| 83 | +} |
0 commit comments