querysub 0.443.0 → 0.446.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -13,7 +13,7 @@ import { getOwnMachineId } from "../-a-auth/certs";
13
13
  import { delay, runInSerial, runInfinitePollCallAtStart } from "socket-function/src/batching";
14
14
  import { lazy } from "socket-function/src/caching";
15
15
  import { errorToUndefinedSilent } from "../errors";
16
- import { commitAndPush, getGitRefInfo, getGitRefLive, getGitURLLive, getGitUncommitted, getLatestRefOnUpstreamBranch, setGitRef } from "../4-deploy/git";
16
+ import { commitAndPush, getGitDiff, getGitRefInfo, getGitRefLive, getGitURLLive, getGitUncommitted, getLatestRefOnUpstreamBranch, setGitRef } from "../4-deploy/git";
17
17
  import { OnServiceChange } from "./machineController";
18
18
  import path from "path";
19
19
  import fs from "fs";
@@ -291,6 +291,21 @@ export class MachineServiceControllerBase {
291
291
  }
292
292
  }
293
293
 
294
+ /** Returns a unified-diff-style summary of all uncommitted changes, for the
295
+ * same repo `commitPushService` / `commitPushAndPublishQuerysub` would commit. */
296
+ public async getGitDiff(config: {
297
+ useQuerysub?: boolean;
298
+ }): Promise<string> {
299
+ let gitDir = ".";
300
+ if (config.useQuerysub) {
301
+ gitDir = path.resolve("../querysub");
302
+ if (!await fsExistsAsync(gitDir)) {
303
+ throw new Error(`Querysub folder does not exist at ${gitDir}`);
304
+ }
305
+ }
306
+ return await getGitDiff(gitDir);
307
+ }
308
+
294
309
  public async commitPushService(commitMessage: string) {
295
310
  if (commitMessage.toLowerCase().includes("querysub")) {
296
311
  let querysubFolder = path.resolve("../querysub");
@@ -448,6 +463,7 @@ export const MachineServiceController = getSyncedController(
448
463
  deleteServiceConfig: {},
449
464
  getGitInfo: {},
450
465
  getGitRefInfo: {},
466
+ getGitDiff: {},
451
467
  commitPushService: {},
452
468
  commitPushAndPublishQuerysub: {},
453
469
  getPendingFunctions: {},
@@ -0,0 +1,231 @@
1
+ /**
2
+ * DebuggerRemote — a small wrapper around a remote Node.js V8 inspector.
3
+ *
4
+ * Connects over the Chrome DevTools Protocol (CDP) and exposes `evaluate()`
5
+ * for running expressions inside the remote process. Connecting is lazy: the
6
+ * constructor kicks it off, and every request awaits it, so callers can just
7
+ * `new DebuggerRemote(...)` and `await remote.evaluate(...)`.
8
+ *
9
+ * Copied from https://github.com/sliftist/debug
10
+ */
11
+ import * as fs from "fs";
12
+
13
+ /** How the remote inspector should be located. */
14
+ export type DebuggerRemoteOptions =
15
+ /** A ready-to-use CDP WebSocket URL (ws://host:port/<uuid>). */
16
+ | { wsUrl: string }
17
+ /** An inspector HTTP port; the live WebSocket URL is looked up. */
18
+ | { port: number; host?: string }
19
+ /** The `debugger-port.json` file written by the target script. */
20
+ | { portFile: string };
21
+
22
+ interface InspectorTarget {
23
+ webSocketDebuggerUrl?: string;
24
+ }
25
+
26
+ interface PortFile {
27
+ port: number;
28
+ url: string;
29
+ pid: number;
30
+ }
31
+
32
+ interface PendingRequest {
33
+ resolve: (result: any) => void;
34
+ reject: (err: Error) => void;
35
+ }
36
+
37
+ const DEFAULT_TIMEOUT_MS = 10_000;
38
+ const CONNECT_MAX_ATTEMPTS = 10;
39
+ const CONNECT_RETRY_DELAY_MS = 1_000;
40
+
41
+ function delay(ms: number): Promise<void> {
42
+ return new Promise((resolve) => setTimeout(resolve, ms));
43
+ }
44
+
45
+ export class DebuggerRemote {
46
+ private ready: Promise<WebSocket>;
47
+ private nextId = 1;
48
+ private pending = new Map<number, PendingRequest>();
49
+ private eventListeners = new Map<string, Set<(params: any) => void>>();
50
+ private closed = false;
51
+
52
+ constructor(private options: DebuggerRemoteOptions) {
53
+ this.ready = this.connect();
54
+ // Avoid an unhandled-rejection warning if nobody awaits a request
55
+ // before the connection itself fails; real callers still see the error.
56
+ this.ready.catch(() => {});
57
+ }
58
+
59
+ /** Evaluate a JS expression inside the remote process and return its value. */
60
+ async evaluate(expression: string): Promise<unknown> {
61
+ const response = await this.send("Runtime.evaluate", {
62
+ expression,
63
+ returnByValue: true,
64
+ awaitPromise: true,
65
+ });
66
+ if (response.exceptionDetails) {
67
+ throw new Error(`Remote threw: ${JSON.stringify(response.exceptionDetails)}`);
68
+ }
69
+ return response.result.value;
70
+ }
71
+
72
+ /**
73
+ * Subscribe to a CDP event notification (e.g. "Debugger.scriptParsed",
74
+ * "Runtime.consoleAPICalled"). Returns an unsubscribe function.
75
+ */
76
+ on(method: string, listener: (params: any) => void): () => void {
77
+ let listeners = this.eventListeners.get(method);
78
+ if (!listeners) {
79
+ listeners = new Set();
80
+ this.eventListeners.set(method, listeners);
81
+ }
82
+ listeners.add(listener);
83
+ return () => {
84
+ listeners!.delete(listener);
85
+ };
86
+ }
87
+
88
+ /** Send a raw CDP command and await its result. */
89
+ send(method: string, params: object = {}, timeoutMs = DEFAULT_TIMEOUT_MS): Promise<any> {
90
+ return this.ready.then(
91
+ (ws) =>
92
+ new Promise<any>((resolve, reject) => {
93
+ if (this.closed) {
94
+ reject(new Error("DebuggerRemote is closed"));
95
+ return;
96
+ }
97
+ const id = this.nextId++;
98
+ const timeout = setTimeout(() => {
99
+ this.pending.delete(id);
100
+ reject(new Error(`Timed out after ${timeoutMs}ms waiting for "${method}"`));
101
+ }, timeoutMs);
102
+ this.pending.set(id, {
103
+ resolve: (result) => {
104
+ clearTimeout(timeout);
105
+ resolve(result);
106
+ },
107
+ reject: (err) => {
108
+ clearTimeout(timeout);
109
+ reject(err);
110
+ },
111
+ });
112
+ ws.send(JSON.stringify({ id, method, params }));
113
+ }),
114
+ );
115
+ }
116
+
117
+ /** Close the connection and reject any in-flight requests. */
118
+ close(): void {
119
+ this.closed = true;
120
+ this.ready.then((ws) => ws.close(), () => {});
121
+ }
122
+
123
+ /**
124
+ * Connect, retrying on any error so `attach` can be started before the
125
+ * target is up (missing port file, inspector not listening, etc.).
126
+ */
127
+ private async connect(): Promise<WebSocket> {
128
+ let lastErr: unknown;
129
+ for (let attempt = 1; attempt <= CONNECT_MAX_ATTEMPTS; attempt++) {
130
+ if (this.closed) throw new Error("DebuggerRemote was closed before connecting");
131
+ try {
132
+ return await this.tryConnect();
133
+ } catch (err) {
134
+ lastErr = err;
135
+ const message = (err as Error)?.message ?? String(err);
136
+ if (attempt < CONNECT_MAX_ATTEMPTS) {
137
+ // stderr, not stdout — callers may use stdout for protocol I/O.
138
+ console.error(
139
+ `[DebuggerRemote] connect attempt ${attempt}/${CONNECT_MAX_ATTEMPTS} ` +
140
+ `failed (${message}); retrying in ${CONNECT_RETRY_DELAY_MS}ms...`,
141
+ );
142
+ await delay(CONNECT_RETRY_DELAY_MS);
143
+ }
144
+ }
145
+ }
146
+ throw new Error(
147
+ `Could not connect to inspector after ${CONNECT_MAX_ATTEMPTS} attempts: ` +
148
+ `${(lastErr as Error)?.message ?? lastErr}`,
149
+ );
150
+ }
151
+
152
+ /** A single connection attempt: resolve the URL and open the WebSocket. */
153
+ private async tryConnect(): Promise<WebSocket> {
154
+ const wsUrl = await this.resolveWsUrl();
155
+ const ws = new WebSocket(wsUrl);
156
+ await new Promise<void>((resolve, reject) => {
157
+ ws.addEventListener("open", () => resolve(), { once: true });
158
+ ws.addEventListener(
159
+ "error",
160
+ () => reject(new Error(`Could not connect to inspector at ${wsUrl}`)),
161
+ { once: true },
162
+ );
163
+ });
164
+ ws.addEventListener("message", (event) => this.onMessage(event));
165
+ ws.addEventListener("close", () => this.onClose());
166
+ return ws;
167
+ }
168
+
169
+ /** Turn the constructor options into a concrete CDP WebSocket URL. */
170
+ private async resolveWsUrl(): Promise<string> {
171
+ const opts = this.options;
172
+ if ("wsUrl" in opts) {
173
+ return opts.wsUrl;
174
+ }
175
+
176
+ let port: number;
177
+ let host = "127.0.0.1";
178
+ if ("portFile" in opts) {
179
+ if (!fs.existsSync(opts.portFile)) {
180
+ throw new Error(
181
+ `Port file not found: ${opts.portFile}\n` +
182
+ `Start the target first with \`yarn test\`.`,
183
+ );
184
+ }
185
+ const info = JSON.parse(fs.readFileSync(opts.portFile, "utf8")) as PortFile;
186
+ port = info.port;
187
+ } else {
188
+ port = opts.port;
189
+ if (opts.host) host = opts.host;
190
+ }
191
+
192
+ // The UUID in the WebSocket URL changes per process, so always look it
193
+ // up fresh from the inspector's HTTP endpoint.
194
+ const res = await fetch(`http://${host}:${port}/json/list`);
195
+ const targets = (await res.json()) as InspectorTarget[];
196
+ const wsUrl = targets[0]?.webSocketDebuggerUrl;
197
+ if (!wsUrl) {
198
+ throw new Error(`No webSocketDebuggerUrl returned by inspector on port ${port}`);
199
+ }
200
+ return wsUrl;
201
+ }
202
+
203
+ private onMessage(event: MessageEvent): void {
204
+ const msg = JSON.parse(event.data as string);
205
+ if (typeof msg.id !== "number") {
206
+ // A CDP event notification: { method, params }, no `id`.
207
+ if (typeof msg.method === "string") {
208
+ const listeners = this.eventListeners.get(msg.method);
209
+ if (listeners) {
210
+ for (const listener of [...listeners]) listener(msg.params ?? {});
211
+ }
212
+ }
213
+ return;
214
+ }
215
+ const pending = this.pending.get(msg.id);
216
+ if (!pending) return;
217
+ this.pending.delete(msg.id);
218
+ if (msg.error) {
219
+ pending.reject(new Error(`CDP error: ${JSON.stringify(msg.error)}`));
220
+ } else {
221
+ pending.resolve(msg.result);
222
+ }
223
+ }
224
+
225
+ private onClose(): void {
226
+ for (const pending of this.pending.values()) {
227
+ pending.reject(new Error("Inspector connection closed"));
228
+ }
229
+ this.pending.clear();
230
+ }
231
+ }