aboutsummaryrefslogtreecommitdiff
path: root/packages/bun-devtools/scripts/client.ts
blob: 3a8fa0b59663d41ea9bd1e43c1bd73c584f2e12c (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
// A DevTools client for JavaScriptCore.

import type { JSC } from "..";

type ClientOptions = {
  url: string | URL;
  event?: (event: JSC.Event<keyof JSC.EventMap>) => void;
  request?: (request: JSC.Request<keyof JSC.RequestMap>) => void;
  response?: (response: JSC.Response<keyof JSC.ResponseMap>) => void;
};

class Client {
  #webSocket: WebSocket;
  #requestId: number;
  #pendingMessages: string[];
  #pendingRequests: Map<number, AbortController>;
  #ready: Promise<void>;

  constructor(options: ClientOptions) {
    this.#webSocket = new WebSocket(options.url);
    this.#requestId = 1;
    this.#pendingMessages = [];
    this.#pendingRequests = new Map();
    this.#ready = new Promise((resolve, reject) => {
      this.#webSocket.addEventListener("open", () => {
        for (const message of this.#pendingMessages) {
          this.#send(message);
        }
        this.#pendingMessages.length = 0;
        resolve();
      });
      this.#webSocket.addEventListener("message", ({ data }) => {
        let response;
        try {
          response = { ...JSON.parse(data) };
        } catch {
          console.error("Received an invalid message:", data);
          return;
        }
        const { id, error, result, method, params } = response;
        if (method && params) {
          options.event?.(response);
        } else if (id && (result || error)) {
          try {
            options.response?.(response);
          } finally {
            const abort = this.#pendingRequests.get(id ?? -1);
            if (!abort) {
              console.error("Received an unexpected message:", response);
              return;
            }
            if (error) {
              abort.abort(new Error(JSON.stringify(error)));
            } else {
              abort.abort(result);
            }
          }
        } else {
          console.error("Received an unexpected message:", response);
        }
      });
      this.#webSocket.addEventListener("error", (error) => {
        reject(error);
      });
      this.#webSocket.addEventListener("close", ({ code, reason = ""}) => {
        reject(new Error(`WebSocket closed: ${code} ${reason}`.trimEnd()));
      });
    });
  }

  get ready(): Promise<void> {
    return this.#ready;
  }

  #send(message: string): void {
    const { readyState } = this.#webSocket;
    if (readyState === WebSocket.OPEN) {
      this.#webSocket.send(message);
    } else if (readyState === WebSocket.CONNECTING) {
      this.#pendingMessages.push(message);
    } else {
      const closed = readyState === WebSocket.CLOSING ? "closing" : "closed";
      throw new Error(`WebSocket is ${closed}`);
    }
  }

  async fetch<T extends keyof JSC.RequestMap>(method: T, params: JSC.Request<T>["params"]): Promise<JSC.Response<T>> {
    const request: JSC.Request<T> = {
      id: this.#requestId++,
      method,
      params,
    };
    return new Promise((resolve, reject) => {
      const abort = new AbortController();
      abort.signal.addEventListener("abort", () => {
        this.#pendingRequests.delete(request.id);
        const { reason } = abort.signal;
        if (reason instanceof Error) {
          reject(reason);
        } else {
          resolve(reason);
        }
      });
      this.#pendingRequests.set(request.id, abort);
      this.#send(JSON.stringify(request));
    });
  }
}

const client = new Client({
  url: "ws://localhost:9229",
  event: (event) => console.log("EVENT:", event),
  request: (request) => console.log("REQUEST:", request),
  response: (response) => console.log("RESPONSE:", response),
});
await client.ready;

while (true) {
  const [method, ...param] = prompt(">")?.split(" ") ?? [];
  if (!method.trim()) {
    continue;
  }
  const params = !param?.length ? {} : JSON.parse(eval(`JSON.stringify(${param.join(" ")})`));
  try {
    await client.fetch(method.trim() as any, params);
  } catch (error) {
    console.error(error);
  }
}