wispjs 2.4.0 → 3.0.1

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.
@@ -1,5 +1,7 @@
1
1
  import stripAnsi from 'strip-ansi';
2
2
  import { WebsocketPool } from "./pool.js";
3
+ import { GitSocket } from "./git.js";
4
+ import { FilesystemSocket } from "./filesystem.js";
3
5
  /**
4
6
  * The primary interface to the Websocket API
5
7
  *
@@ -11,6 +13,8 @@ export class WispSocket {
11
13
  this.api = api;
12
14
  this.ghToken = ghToken;
13
15
  this.consoleCallbacks = [];
16
+ this.Git = new GitSocket(this);
17
+ this.Filesystem = new FilesystemSocket(this);
14
18
  }
15
19
  /**
16
20
  * Sets a callback to run on the Websocket Info before saving the details.
@@ -43,7 +47,16 @@ export class WispSocket {
43
47
  if (!this.url || !this.token) {
44
48
  throw new Error("Attempted to create a pool without a URL or token");
45
49
  }
46
- this.pool = new WebsocketPool(this.url, this.token);
50
+ // Provider used by the pool to refresh the (short-lived) token when a
51
+ // worker needs to reconnect.
52
+ const refreshDetails = async () => {
53
+ await this.setDetails();
54
+ return { url: this.url, token: this.token };
55
+ };
56
+ // The Origin header is the panel the connection originates from, which
57
+ // is always the API domain.
58
+ const origin = `https://${this.api.domain}`;
59
+ this.pool = new WebsocketPool(this.url, this.token, origin, refreshDetails);
47
60
  }
48
61
  /**
49
62
  * Requests and saves the Websocket details from the API
@@ -58,7 +71,7 @@ export class WispSocket {
58
71
  }
59
72
  this.url = websocketInfo.url;
60
73
  this.token = websocketInfo.token;
61
- this.logger.info("Got Websocket Details.", this.url, this.token);
74
+ this.logger.info(`Got Websocket Details. ${this.url} - ${this.token}`);
62
75
  }
63
76
  catch (e) {
64
77
  this.logger.error(`Failed to get websocket details: ${e}`);
@@ -87,178 +100,112 @@ export class WispSocket {
87
100
  }
88
101
  }
89
102
  /**
90
- * Searches all file contents for the given query
103
+ * Runs a job on an available pool worker, handing it the worker's socket
104
+ * and logger. Used for event-based flows (e.g. file search) that don't fit
105
+ * the {@link request} request/response protocol.
91
106
  *
92
- * @param query The query string to search for
93
- * @param timeout How long to wait (in ms) for results before timing out
107
+ * @param work The job to run; receives the worker and returns a Promise
94
108
  *
95
- * @public
109
+ * @internal
96
110
  */
97
- async filesearch(query, timeout = 10000) {
98
- this.logger.info("Running filesearch with: ", query);
111
+ async runWorker(work) {
99
112
  await this.verifyPool();
100
- return await this.pool.run((worker) => {
101
- const socket = worker.socket;
102
- const logger = worker.logger;
103
- logger.log("Running filesearch:", query);
104
- return new Promise((resolve, reject) => {
105
- const timeoutObj = setTimeout(() => {
106
- socket.off("filesearch-results");
107
- logger.error("Rejected filesearch: 'Timeout'");
108
- reject();
109
- }, timeout);
110
- socket.once("filesearch-results", (data) => {
111
- clearTimeout(timeoutObj);
112
- resolve(data);
113
- });
114
- socket.emit("filesearch-start", query);
115
- });
116
- });
113
+ return await this.pool.run(work);
117
114
  }
118
115
  /**
119
- * Performs a git pull operation on the given directory
120
- *
121
- * @param dir The full directory path to perform a pull on
122
- * @param timeout In milliseconds, how long to wait before timing out
116
+ * Generates a short, unique request id for {@link request} correlation.
123
117
  *
124
- * @public
118
+ * @internal
125
119
  */
126
- async gitPull(dir, useAuth = false, timeout = 10000) {
127
- await this.verifyPool();
128
- const pullResult = await this.pool.run((worker) => {
129
- const socket = worker.socket;
130
- const logger = worker.logger;
131
- logger.log("Running gitPull:", dir);
132
- return new Promise((resolve, reject) => {
133
- let isPrivate = false;
134
- let finished;
135
- const timeoutObj = setTimeout(() => {
136
- logger.error("Rejected gitPull: 'Timeout'");
137
- finished(false, "Timeout");
138
- }, timeout);
139
- finished = (success, output) => {
140
- socket.removeAllListeners("git-pull");
141
- socket.removeAllListeners("git-error");
142
- socket.removeAllListeners("git-success");
143
- const result = {
144
- output: output,
145
- isPrivate: isPrivate
146
- };
147
- if (success) {
148
- resolve(result);
149
- }
150
- else {
151
- logger.error("Rejected gitPull:", dir, output);
152
- reject(output);
153
- }
154
- clearTimeout(timeoutObj);
155
- };
156
- const sendRequest = (includeAuth = false) => {
157
- const data = { dir: dir };
158
- if (includeAuth) {
159
- if (!this.ghToken) {
160
- logger.error("No GitHub token set, can't authenticate");
161
- return finished(false, "Authentication is required, but no GitHub token was set. Can't pull!");
162
- }
163
- isPrivate = true;
164
- data.authkey = this.ghToken;
165
- }
166
- socket.emit("git-pull", data);
167
- };
168
- socket.once("git-pull", (data) => {
169
- logger.log(`Updating ${data}`);
170
- });
171
- socket.once("git-success", (commit) => {
172
- logger.log(`Addon updated to ${commit}`);
173
- if (!commit) {
174
- logger.log("No commit given!");
175
- }
176
- finished(true, commit || "");
177
- });
178
- socket.on("git-error", (message) => {
179
- if (message === "Remote authentication required but no callback set") {
180
- logger.log(`Remote authentication required, trying again with authkey: ${dir}`);
181
- sendRequest(true);
182
- }
183
- else {
184
- logger.log(`Error updating addon: ${message}`);
185
- finished(false, message);
186
- }
187
- });
188
- sendRequest(useAuth);
189
- });
190
- });
191
- return pullResult;
120
+ generateReqId() {
121
+ return Math.random().toString(36).slice(2, 14);
192
122
  }
193
123
  /**
194
- * Clones a new Repo to the given directory
124
+ * Issues a correlated `fs:request` to the backend and awaits its outcome.
125
+ *
126
+ * This is the low-level entry point for the git/filesystem protocol. The
127
+ * client emits `fs:request` with a JSON-string payload (`{ req, op, ...params }`),
128
+ * and the backend replies with one of three events, all correlated by `req`
129
+ * and all carrying a JSON-string `args[0]`:
130
+ *
131
+ * - `fs:result` — success; resolves with the parsed `data`
132
+ * - `fs:error` — failure; rejects with an {@link FsError} (plus collected `output`)
133
+ * - `fs:progress` — streaming stdout/stderr/progress lines (collected for error context)
195
134
  *
196
- * @param url The HTTPS URL of the repository
197
- * @param dir The full path of the directory to clone the repository to
198
- * @param branch The branch of the repository to clone
199
- * @param timeout In milliseconds, how long to wait before timing out
135
+ * Typed convenience wrappers live on {@link GitSocket} ({@link WispSocket.Git})
136
+ * and {@link FilesystemSocket} ({@link WispSocket.Filesystem}); call this
137
+ * directly for ops that don't have a typed wrapper yet.
138
+ *
139
+ * @example
140
+ * ```js
141
+ * const status = await wisp.socket.request("git-status", { directory: "/garrysmod/addons/acf-3" })
142
+ * ```
143
+ *
144
+ * @param op The operation name (e.g. `git-pull`, `list`)
145
+ * @param params Additional payload fields merged into the request (e.g. `{ directory }`)
146
+ * @param timeout In milliseconds, how long to wait for the result
200
147
  *
201
148
  * @public
202
149
  */
203
- async gitClone(url, dir, branch, timeout = 20000) {
150
+ async request(op, params = {}, timeout = 10000) {
204
151
  await this.verifyPool();
205
152
  return await this.pool.run((worker) => {
206
153
  const socket = worker.socket;
207
154
  const logger = worker.logger;
208
- logger.log("Running gitClone:", url, dir, branch);
155
+ const req = this.generateReqId();
156
+ logger.log(`Running fsRequest: ${op}`, params);
209
157
  return new Promise((resolve, reject) => {
210
- let isPrivate = false;
211
- let finished;
212
- const timeoutObj = setTimeout(() => {
213
- logger.error("Rejected gitClone: 'Timeout'");
214
- finished(false, "Timeout");
215
- }, timeout);
216
- finished = (success, message) => {
217
- socket.removeAllListeners("git-clone");
218
- socket.removeAllListeners("git-error");
219
- socket.removeAllListeners("git-success");
220
- if (success) {
221
- const result = {
222
- isPrivate: isPrivate
223
- };
224
- resolve(result);
158
+ const progress = [];
159
+ let resultHandler;
160
+ let errorHandler;
161
+ let progressHandler;
162
+ const parse = (raw) => {
163
+ try {
164
+ return JSON.parse(raw);
225
165
  }
226
- else {
227
- logger.error("Rejected gitClone:", url, dir, branch, message);
228
- reject(message);
166
+ catch {
167
+ logger.error(`Failed to parse fs event for ${op}`, raw);
168
+ return undefined;
229
169
  }
170
+ };
171
+ const cleanup = () => {
172
+ socket.off("fs:result", resultHandler);
173
+ socket.off("fs:error", errorHandler);
174
+ socket.off("fs:progress", progressHandler);
230
175
  clearTimeout(timeoutObj);
231
176
  };
232
- const sendRequest = (includeAuth = false) => {
233
- const data = { dir: dir, url: url, branch: branch };
234
- if (includeAuth) {
235
- if (!this.ghToken) {
236
- logger.error("No GitHub token set, can't authenticate");
237
- return finished(false, "Authentication is required, but no GitHub token was set. Can't clone!");
238
- }
239
- isPrivate = true;
240
- data.authkey = this.ghToken;
241
- }
242
- socket.emit("git-clone", data);
177
+ const timeoutObj = setTimeout(() => {
178
+ cleanup();
179
+ logger.error(`fsRequest timed out: ${op} (${req})`);
180
+ reject(new Error(`fsRequest timed out: ${op}`));
181
+ }, timeout);
182
+ progressHandler = (raw) => {
183
+ const p = parse(raw);
184
+ if (!p || p.req !== req)
185
+ return;
186
+ const line = p.line ?? "";
187
+ progress.push(line);
188
+ logger.debug(`[${op}] ${p.kind}: ${line}`);
243
189
  };
244
- socket.once("git-clone", (data) => {
245
- logger.log(`Cloning ${data}`);
246
- });
247
- socket.once("git-success", () => {
248
- logger.log("Project successfully cloned");
249
- finished(true);
250
- });
251
- socket.on("git-error", (message) => {
252
- if (message === "Remote authentication required but no callback set") {
253
- logger.log(`Remote authentication required, trying again with authkey: ${dir}`);
254
- sendRequest(true);
255
- }
256
- else {
257
- logger.log("Error cloning repo:", url, dir, branch, message);
258
- finished(false, message);
259
- }
260
- });
261
- sendRequest();
190
+ resultHandler = (raw) => {
191
+ const p = parse(raw);
192
+ if (!p || p.req !== req)
193
+ return;
194
+ cleanup();
195
+ resolve(p.data);
196
+ };
197
+ errorHandler = (raw) => {
198
+ const p = parse(raw);
199
+ if (!p || p.req !== req)
200
+ return;
201
+ cleanup();
202
+ logger.error(`fsRequest error: ${op} - ${p.code}: ${p.message}`);
203
+ reject({ ...p, output: progress.join("\n") });
204
+ };
205
+ socket.on("fs:result", resultHandler);
206
+ socket.on("fs:error", errorHandler);
207
+ socket.on("fs:progress", progressHandler);
208
+ socket.emit("fs:request", JSON.stringify({ req, op, ...params }));
262
209
  });
263
210
  });
264
211
  }
@@ -273,9 +220,13 @@ export class WispSocket {
273
220
  const logger = worker.logger;
274
221
  logger.log("Running setupConsoleListener");
275
222
  return new Promise((resolve) => {
276
- worker.socket.on("console", (data) => {
277
- const line = data.line;
223
+ const cleanup = () => {
224
+ worker.socket.off("console output", handleLine);
225
+ worker.socket.off("disconnect", handleDisconnect);
226
+ };
227
+ const handleLine = (line) => {
278
228
  if (this.consoleCallbacks.length == 0) {
229
+ cleanup();
279
230
  return resolve();
280
231
  }
281
232
  this.consoleCallbacks.forEach((callback) => {
@@ -286,7 +237,16 @@ export class WispSocket {
286
237
  logger.error("Failed to run console callback", e);
287
238
  }
288
239
  });
289
- });
240
+ };
241
+ const handleDisconnect = () => {
242
+ cleanup();
243
+ resolve();
244
+ if (this.consoleCallbacks.length > 0) {
245
+ this.setupConsoleListener();
246
+ }
247
+ };
248
+ worker.socket.on("console output", handleLine);
249
+ worker.socket.once("disconnect", handleDisconnect);
290
250
  });
291
251
  });
292
252
  });
@@ -363,17 +323,16 @@ export class WispSocket {
363
323
  let callback;
364
324
  const timeoutObj = setTimeout(() => {
365
325
  logger.error(`Command timed out current output: '${output}'`);
366
- socket.off("console", callback);
326
+ socket.off("console output", callback);
367
327
  logger.log("Rejected sendCommandNonce 'Timeout'", nonce, command);
368
328
  reject("Timeout");
369
329
  }, timeout);
370
- callback = (data) => {
371
- const line = data.line;
330
+ callback = (line) => {
372
331
  const clean = stripAnsi(line);
373
332
  if (clean.startsWith(nonce)) {
374
333
  const message = clean.slice(nonce.length);
375
334
  if (message === "Done.") {
376
- socket.off("console", callback);
335
+ socket.off("console output", callback);
377
336
  clearTimeout(timeoutObj);
378
337
  resolve(output);
379
338
  }
@@ -383,7 +342,7 @@ export class WispSocket {
383
342
  }
384
343
  }
385
344
  };
386
- socket.on("console", callback);
345
+ socket.on("console output", callback);
387
346
  socket.emit("send command", command);
388
347
  });
389
348
  });