wispjs 2.3.6 → 3.0.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.
- package/README.md +1 -1
- package/dist/wisp_api/index.d.ts +6 -0
- package/dist/wisp_api/index.js +8 -0
- package/dist/wisp_socket/filesystem.d.ts +60 -0
- package/dist/wisp_socket/filesystem.js +97 -0
- package/dist/wisp_socket/git.d.ts +57 -0
- package/dist/wisp_socket/git.js +76 -0
- package/dist/wisp_socket/index.d.ts +37 -18
- package/dist/wisp_socket/index.js +106 -159
- package/dist/wisp_socket/pool.d.ts +192 -48
- package/dist/wisp_socket/pool.js +132 -33
- package/dist/wisp_socket/ws_adapter.d.ts +72 -0
- package/dist/wisp_socket/ws_adapter.js +130 -0
- package/package.json +5 -3
|
@@ -1,9 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Logger = {
|
|
1
|
+
import { WispWebSocket } from "./ws_adapter.js";
|
|
2
|
+
export type Logger = {
|
|
3
3
|
log(...args: any[]): void;
|
|
4
4
|
error(...args: any[]): void;
|
|
5
5
|
debug(...args: any[]): void;
|
|
6
6
|
};
|
|
7
|
+
/**
|
|
8
|
+
* The minimal worker surface handed to a job run via {@link WispSocket.runWorker}:
|
|
9
|
+
* the connected socket and a prefixed logger.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export interface SocketWorker {
|
|
14
|
+
socket: WispWebSocket;
|
|
15
|
+
logger: Logger;
|
|
16
|
+
}
|
|
7
17
|
/**
|
|
8
18
|
* The struct used to define the events that can be sent from the server to the client
|
|
9
19
|
*
|
|
@@ -11,13 +21,14 @@ type Logger = {
|
|
|
11
21
|
*/
|
|
12
22
|
export interface ServerToClientEvents {
|
|
13
23
|
"error": (message: string) => void;
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"
|
|
20
|
-
"
|
|
24
|
+
"auth success": () => void;
|
|
25
|
+
"console output": (line: string) => void;
|
|
26
|
+
"status": (state: string) => void;
|
|
27
|
+
"stats": (statsJson: string) => void;
|
|
28
|
+
"filesearch results": (json: string) => void;
|
|
29
|
+
"fs:result": (json: string) => void;
|
|
30
|
+
"fs:error": (json: string) => void;
|
|
31
|
+
"fs:progress": (json: string) => void;
|
|
21
32
|
"initial status": (message: any) => void;
|
|
22
33
|
}
|
|
23
34
|
/**
|
|
@@ -27,72 +38,150 @@ export interface ServerToClientEvents {
|
|
|
27
38
|
*/
|
|
28
39
|
export interface ClientToServerEvents {
|
|
29
40
|
"auth": (token: string) => void;
|
|
30
|
-
"filesearch
|
|
31
|
-
"
|
|
32
|
-
"git-pull": (data: GitPullData) => void;
|
|
41
|
+
"filesearch start": (json: string) => void;
|
|
42
|
+
"fs:request": (json: string) => void;
|
|
33
43
|
"send command": (command: string) => void;
|
|
34
44
|
}
|
|
35
45
|
/**
|
|
36
|
-
*
|
|
46
|
+
* Return struct after finishing a Git Clone action
|
|
37
47
|
*
|
|
38
|
-
* @param
|
|
39
|
-
* @param
|
|
48
|
+
* @param folder_name The name of the folder the repo was cloned into
|
|
49
|
+
* @param commit The HEAD commit hash after the clone
|
|
50
|
+
* @param branch The branch that was cloned
|
|
40
51
|
*
|
|
41
52
|
* @internal
|
|
42
53
|
*/
|
|
43
|
-
export interface
|
|
44
|
-
|
|
45
|
-
|
|
54
|
+
export interface GitCloneResult {
|
|
55
|
+
folder_name: string;
|
|
56
|
+
commit: string;
|
|
57
|
+
branch: string;
|
|
46
58
|
}
|
|
47
59
|
/**
|
|
48
|
-
* Struct
|
|
60
|
+
* Struct returned after a Git Pull action finishes.
|
|
49
61
|
*
|
|
50
|
-
* @param
|
|
51
|
-
* @param
|
|
52
|
-
* @param
|
|
53
|
-
* @param
|
|
62
|
+
* @param commit The HEAD commit hash after the pull
|
|
63
|
+
* @param branch The branch that was pulled
|
|
64
|
+
* @param files_changed How many files changed (0 when already up to date)
|
|
65
|
+
* @param up_to_date Whether the repo was already up to date
|
|
54
66
|
*
|
|
55
67
|
* @internal
|
|
56
68
|
*/
|
|
57
|
-
export interface
|
|
58
|
-
|
|
59
|
-
url: string;
|
|
69
|
+
export interface GitPullResult {
|
|
70
|
+
commit: string;
|
|
60
71
|
branch: string;
|
|
61
|
-
|
|
72
|
+
files_changed: number;
|
|
73
|
+
up_to_date: boolean;
|
|
62
74
|
}
|
|
63
75
|
/**
|
|
64
|
-
*
|
|
76
|
+
* Payload for an `fs:request`. Sent as a JSON string; `op`-specific fields go
|
|
77
|
+
* alongside `req` and `op`.
|
|
65
78
|
*
|
|
66
|
-
* @
|
|
79
|
+
* @internal
|
|
80
|
+
*/
|
|
81
|
+
export interface FsRequest {
|
|
82
|
+
req: string;
|
|
83
|
+
op: string;
|
|
84
|
+
directory?: string;
|
|
85
|
+
[key: string]: any;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The `fs:result` success envelope; `data` carries the op-specific payload.
|
|
67
89
|
*
|
|
68
90
|
* @internal
|
|
69
91
|
*/
|
|
70
|
-
export interface
|
|
71
|
-
|
|
92
|
+
export interface FsResult<T = any> {
|
|
93
|
+
req: string;
|
|
94
|
+
data?: T;
|
|
72
95
|
}
|
|
73
96
|
/**
|
|
74
|
-
*
|
|
97
|
+
* The `fs:error` envelope. Errors arrive as a separate event from `fs:result`,
|
|
98
|
+
* correlated by `req` (e.g. `code: "auth_required"` / `"auth_invalid"`).
|
|
75
99
|
*
|
|
76
|
-
* @
|
|
77
|
-
|
|
100
|
+
* @internal
|
|
101
|
+
*/
|
|
102
|
+
export interface FsError {
|
|
103
|
+
req: string;
|
|
104
|
+
code: string;
|
|
105
|
+
message: string;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The parsed `fs:progress` envelope, correlated by `req`. Comes in two shapes:
|
|
109
|
+
* text lines (`kind: "stdout" | "stderr"`, `line`) and structured progress
|
|
110
|
+
* (`kind: "progress"` with `phase`/`current`/`total`/`percent`).
|
|
78
111
|
*
|
|
79
112
|
* @internal
|
|
80
113
|
*/
|
|
81
|
-
export interface
|
|
82
|
-
|
|
83
|
-
|
|
114
|
+
export interface FsProgress {
|
|
115
|
+
req: string;
|
|
116
|
+
kind: "stdout" | "stderr" | "progress" | string;
|
|
117
|
+
line?: string;
|
|
118
|
+
phase?: string;
|
|
119
|
+
current?: number;
|
|
120
|
+
total?: number;
|
|
121
|
+
percent?: number;
|
|
84
122
|
}
|
|
85
123
|
/**
|
|
86
|
-
*
|
|
124
|
+
* Result of a `git-status` op. Repo-specific fields are absent when
|
|
125
|
+
* `is_repo` is false.
|
|
87
126
|
*
|
|
88
|
-
* @
|
|
89
|
-
|
|
127
|
+
* @internal
|
|
128
|
+
*/
|
|
129
|
+
export interface GitStatusResult {
|
|
130
|
+
is_repo: boolean;
|
|
131
|
+
ahead: number;
|
|
132
|
+
behind: number;
|
|
133
|
+
dirty: boolean;
|
|
134
|
+
repo_root?: string;
|
|
135
|
+
branch?: string;
|
|
136
|
+
commit?: string;
|
|
137
|
+
remote_url?: string;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* A single entry in a `list` op result.
|
|
90
141
|
*
|
|
91
142
|
* @internal
|
|
92
143
|
*/
|
|
93
|
-
export interface
|
|
94
|
-
|
|
95
|
-
|
|
144
|
+
export interface FileEntry {
|
|
145
|
+
name: string;
|
|
146
|
+
created: string;
|
|
147
|
+
modified: string;
|
|
148
|
+
mode: string;
|
|
149
|
+
mode_bits: string;
|
|
150
|
+
size: number;
|
|
151
|
+
directory: boolean;
|
|
152
|
+
file: boolean;
|
|
153
|
+
symlink: boolean;
|
|
154
|
+
mime: string;
|
|
155
|
+
child_count?: number;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Options for a `list` op.
|
|
159
|
+
*
|
|
160
|
+
* @internal
|
|
161
|
+
*/
|
|
162
|
+
export interface ListOptions {
|
|
163
|
+
search?: string;
|
|
164
|
+
page?: number;
|
|
165
|
+
perPage?: number;
|
|
166
|
+
sort?: string;
|
|
167
|
+
sortDir?: "asc" | "desc";
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Result of a `list` op: a paginated directory listing.
|
|
171
|
+
*
|
|
172
|
+
* @internal
|
|
173
|
+
*/
|
|
174
|
+
export interface ListResult {
|
|
175
|
+
data: FileEntry[];
|
|
176
|
+
meta: {
|
|
177
|
+
pagination: {
|
|
178
|
+
count: number;
|
|
179
|
+
currentPage: number;
|
|
180
|
+
perPage: number;
|
|
181
|
+
total: number;
|
|
182
|
+
totalPages: number;
|
|
183
|
+
};
|
|
184
|
+
};
|
|
96
185
|
}
|
|
97
186
|
/**
|
|
98
187
|
* An individual filesearch result
|
|
@@ -123,10 +212,33 @@ export interface FilesearchResults {
|
|
|
123
212
|
tooMany: boolean;
|
|
124
213
|
}
|
|
125
214
|
/**
|
|
126
|
-
*
|
|
215
|
+
* Result of an op that just reports success (e.g. `mkdir`).
|
|
216
|
+
*
|
|
217
|
+
* @internal
|
|
218
|
+
*/
|
|
219
|
+
export interface FsOkResult {
|
|
220
|
+
ok: boolean;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Result of a `conflicts` op — the subset of the given paths that already
|
|
224
|
+
* exist / would conflict.
|
|
225
|
+
*
|
|
226
|
+
* @internal
|
|
227
|
+
*/
|
|
228
|
+
export interface ConflictsResult {
|
|
229
|
+
conflicts: string[];
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* The websocket type used throughout the pool.
|
|
233
|
+
*
|
|
234
|
+
* The backend no longer speaks Socket.IO, so this is our {@link WispWebSocket}
|
|
235
|
+
* adapter rather than a Socket.IO `Socket`. The {@link ServerToClientEvents}
|
|
236
|
+
* and {@link ClientToServerEvents} interfaces above document the event names
|
|
237
|
+
* but are no longer enforced by the adapter's looser `(...args: any[])` API.
|
|
238
|
+
*
|
|
127
239
|
* @internal
|
|
128
240
|
*/
|
|
129
|
-
export type WispWebsocket =
|
|
241
|
+
export type WispWebsocket = WispWebSocket;
|
|
130
242
|
/**
|
|
131
243
|
* A single worker in the Websocket Pool
|
|
132
244
|
*
|
|
@@ -137,7 +249,9 @@ interface PoolWorker {
|
|
|
137
249
|
socket: WispWebsocket;
|
|
138
250
|
idx: number;
|
|
139
251
|
token: string;
|
|
140
|
-
|
|
252
|
+
url: string;
|
|
253
|
+
connected: boolean;
|
|
254
|
+
busy: boolean;
|
|
141
255
|
done: boolean;
|
|
142
256
|
logger: Logger;
|
|
143
257
|
}
|
|
@@ -149,11 +263,36 @@ interface PoolWorker {
|
|
|
149
263
|
* @internal
|
|
150
264
|
*/
|
|
151
265
|
declare class PoolWorker {
|
|
266
|
+
private intentionalDisconnect;
|
|
267
|
+
private reconnectAttempts;
|
|
268
|
+
private readonly maxReconnectAttempts;
|
|
152
269
|
constructor(pool: WebsocketPool);
|
|
270
|
+
private createSocket;
|
|
271
|
+
private start;
|
|
153
272
|
connect(): Promise<void>;
|
|
273
|
+
private handleDisconnect;
|
|
154
274
|
disconnect(): Promise<void>;
|
|
155
275
|
private processWork;
|
|
156
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* A queued unit of work plus a hook to reject it if it can never run (e.g. the
|
|
279
|
+
* pool has no live workers, or it waited too long to be picked up).
|
|
280
|
+
*
|
|
281
|
+
* @internal
|
|
282
|
+
*/
|
|
283
|
+
interface QueueItem {
|
|
284
|
+
task: (worker: PoolWorker) => Promise<void>;
|
|
285
|
+
reject: (reason?: any) => void;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Fetches fresh websocket details (url + token) for reconnects.
|
|
289
|
+
*
|
|
290
|
+
* @internal
|
|
291
|
+
*/
|
|
292
|
+
export type DetailsProvider = () => Promise<{
|
|
293
|
+
url: string;
|
|
294
|
+
token: string;
|
|
295
|
+
}>;
|
|
157
296
|
/**
|
|
158
297
|
* Struct used to manage a pool of WebSocket workers
|
|
159
298
|
*/
|
|
@@ -161,9 +300,12 @@ export interface WebsocketPool {
|
|
|
161
300
|
workers: PoolWorker[];
|
|
162
301
|
token: string;
|
|
163
302
|
url: string;
|
|
303
|
+
origin: string;
|
|
164
304
|
maxWorkers: number;
|
|
305
|
+
acquireTimeout: number;
|
|
306
|
+
refreshDetails?: DetailsProvider;
|
|
165
307
|
logger: Logger;
|
|
166
|
-
queue:
|
|
308
|
+
queue: QueueItem[];
|
|
167
309
|
}
|
|
168
310
|
/**
|
|
169
311
|
* A pool of {@link PoolWorker}s
|
|
@@ -177,8 +319,10 @@ export interface WebsocketPool {
|
|
|
177
319
|
* @internal
|
|
178
320
|
*/
|
|
179
321
|
export declare class WebsocketPool {
|
|
180
|
-
constructor(url: string, token: string);
|
|
322
|
+
constructor(url: string, token: string, origin: string, refreshDetails?: DetailsProvider);
|
|
181
323
|
getWork(): ((worker: PoolWorker) => Promise<any>) | undefined;
|
|
324
|
+
private allWorkersDone;
|
|
325
|
+
onWorkerDone(): void;
|
|
182
326
|
disconnect(): Promise<void>;
|
|
183
327
|
run(work: (worker: PoolWorker) => Promise<any>): Promise<any>;
|
|
184
328
|
}
|
package/dist/wisp_socket/pool.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { WispWebSocket } from "./ws_adapter.js";
|
|
2
2
|
const WISP_DEBUG = process.env.WISP_DEBUG === "true";
|
|
3
3
|
/**
|
|
4
4
|
* A single Worker within a {@link WebsocketPool}
|
|
@@ -9,17 +9,16 @@ const WISP_DEBUG = process.env.WISP_DEBUG === "true";
|
|
|
9
9
|
*/
|
|
10
10
|
class PoolWorker {
|
|
11
11
|
constructor(pool) {
|
|
12
|
+
this.intentionalDisconnect = false;
|
|
13
|
+
this.reconnectAttempts = 0;
|
|
14
|
+
this.maxReconnectAttempts = 3;
|
|
12
15
|
this.pool = pool;
|
|
13
|
-
this.
|
|
16
|
+
this.connected = false;
|
|
17
|
+
this.busy = false;
|
|
14
18
|
this.done = false;
|
|
15
19
|
this.idx = pool.workers.length;
|
|
16
20
|
this.token = pool.token;
|
|
17
|
-
this.
|
|
18
|
-
forceNew: true,
|
|
19
|
-
transports: ["websocket"],
|
|
20
|
-
addTrailingSlash: true,
|
|
21
|
-
autoConnect: false
|
|
22
|
-
});
|
|
21
|
+
this.url = pool.url;
|
|
23
22
|
const logPrefix = `[Worker #${this.idx}]`;
|
|
24
23
|
this.logger = {
|
|
25
24
|
log: (...args) => console.log(logPrefix, args),
|
|
@@ -30,7 +29,22 @@ class PoolWorker {
|
|
|
30
29
|
console.debug(logPrefix, args);
|
|
31
30
|
}
|
|
32
31
|
};
|
|
33
|
-
this.
|
|
32
|
+
this.start();
|
|
33
|
+
this.processWork();
|
|
34
|
+
}
|
|
35
|
+
createSocket() {
|
|
36
|
+
this.socket = new WispWebSocket(this.url, {
|
|
37
|
+
origin: this.pool.origin
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
this.createSocket();
|
|
42
|
+
this.connect()
|
|
43
|
+
.then(() => this.logger.log("Connection established"))
|
|
44
|
+
.catch((err) => {
|
|
45
|
+
this.logger.error("Connection failed", err);
|
|
46
|
+
this.handleDisconnect("connection failure");
|
|
47
|
+
});
|
|
34
48
|
}
|
|
35
49
|
connect() {
|
|
36
50
|
const socket = this.socket;
|
|
@@ -40,11 +54,10 @@ class PoolWorker {
|
|
|
40
54
|
return new Promise((resolve, reject) => {
|
|
41
55
|
const timeout = setTimeout(() => {
|
|
42
56
|
logger.error("Socket didn't connect in time");
|
|
43
|
-
reject("Connection Timeout");
|
|
57
|
+
reject(new Error("Connection Timeout"));
|
|
44
58
|
}, 10000);
|
|
45
59
|
socket.on("connect", () => {
|
|
46
|
-
logger.log("Connected to WebSocket");
|
|
47
|
-
logger.log("Emitting:", "auth", this.token);
|
|
60
|
+
logger.log("Connected to WebSocket, authenticating");
|
|
48
61
|
socket.emit("auth", this.token);
|
|
49
62
|
});
|
|
50
63
|
socket.on("error", (reason) => {
|
|
@@ -52,28 +65,65 @@ class PoolWorker {
|
|
|
52
65
|
});
|
|
53
66
|
socket.on("connect_error", (error) => {
|
|
54
67
|
logger.error(`WebSocket Connect error: ${error.toString()}`);
|
|
55
|
-
this.done = true;
|
|
56
68
|
clearTimeout(timeout);
|
|
57
|
-
reject(
|
|
69
|
+
reject(error);
|
|
58
70
|
});
|
|
59
71
|
socket.on("disconnect", (reason) => {
|
|
60
72
|
logger.log(`Disconnected from WebSocket: ${reason}`);
|
|
73
|
+
this.connected = false;
|
|
74
|
+
this.handleDisconnect(reason);
|
|
61
75
|
});
|
|
62
|
-
socket.on("
|
|
76
|
+
socket.on("auth success", () => {
|
|
63
77
|
logger.log("Auth success");
|
|
64
|
-
this.
|
|
78
|
+
this.reconnectAttempts = 0;
|
|
79
|
+
this.connected = true;
|
|
65
80
|
clearTimeout(timeout);
|
|
66
81
|
resolve();
|
|
67
82
|
});
|
|
68
83
|
socket.connect();
|
|
69
84
|
});
|
|
70
85
|
}
|
|
86
|
+
// Recovers from an unexpected disconnect by reconnecting with a freshly
|
|
87
|
+
// fetched token (the JWT is short-lived). After maxReconnectAttempts the
|
|
88
|
+
// worker is marked dead so the pool stops handing it work.
|
|
89
|
+
async handleDisconnect(reason) {
|
|
90
|
+
this.connected = false;
|
|
91
|
+
if (this.intentionalDisconnect || this.done) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
95
|
+
this.logger.error(`Giving up after ${this.reconnectAttempts} reconnect attempts (${reason})`);
|
|
96
|
+
this.done = true;
|
|
97
|
+
this.pool.onWorkerDone();
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
this.reconnectAttempts++;
|
|
101
|
+
const backoff = Math.min(1000 * this.reconnectAttempts, 5000);
|
|
102
|
+
this.logger.log(`Reconnecting (attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}) in ${backoff}ms`);
|
|
103
|
+
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
104
|
+
if (this.intentionalDisconnect || this.done) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (this.pool.refreshDetails) {
|
|
108
|
+
try {
|
|
109
|
+
const details = await this.pool.refreshDetails();
|
|
110
|
+
this.url = details.url;
|
|
111
|
+
this.token = details.token;
|
|
112
|
+
}
|
|
113
|
+
catch (e) {
|
|
114
|
+
this.logger.error("Failed to refresh websocket details for reconnect", e);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
this.start();
|
|
118
|
+
}
|
|
71
119
|
disconnect() {
|
|
72
|
-
this.
|
|
73
|
-
|
|
120
|
+
this.intentionalDisconnect = true;
|
|
121
|
+
this.connected = false;
|
|
122
|
+
return new Promise((resolve) => {
|
|
74
123
|
const timeout = setTimeout(() => {
|
|
75
124
|
this.logger.error("Socket didn't disconnect in time");
|
|
76
|
-
|
|
125
|
+
this.done = true;
|
|
126
|
+
resolve();
|
|
77
127
|
}, 5000);
|
|
78
128
|
this.socket.once("disconnect", () => {
|
|
79
129
|
this.done = true;
|
|
@@ -85,13 +135,13 @@ class PoolWorker {
|
|
|
85
135
|
}
|
|
86
136
|
async processWork() {
|
|
87
137
|
while (!this.done) {
|
|
88
|
-
if (!this.
|
|
138
|
+
if (!this.connected || this.busy) {
|
|
89
139
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
90
140
|
continue;
|
|
91
141
|
}
|
|
92
142
|
const work = this.pool.getWork();
|
|
93
143
|
if (work) {
|
|
94
|
-
this.
|
|
144
|
+
this.busy = true;
|
|
95
145
|
try {
|
|
96
146
|
this.logger.debug("Running my work");
|
|
97
147
|
await work(this);
|
|
@@ -102,7 +152,7 @@ class PoolWorker {
|
|
|
102
152
|
this.logger.error(e);
|
|
103
153
|
}
|
|
104
154
|
finally {
|
|
105
|
-
this.
|
|
155
|
+
this.busy = false;
|
|
106
156
|
}
|
|
107
157
|
}
|
|
108
158
|
else {
|
|
@@ -123,11 +173,15 @@ class PoolWorker {
|
|
|
123
173
|
* @internal
|
|
124
174
|
*/
|
|
125
175
|
export class WebsocketPool {
|
|
126
|
-
constructor(url, token) {
|
|
176
|
+
constructor(url, token, origin, refreshDetails) {
|
|
127
177
|
const envMaxWorkers = process.env.WISP_MAX_WORKERS;
|
|
128
178
|
this.maxWorkers = envMaxWorkers ? parseInt(envMaxWorkers) : 5;
|
|
179
|
+
const envAcquire = process.env.WISP_ACQUIRE_TIMEOUT;
|
|
180
|
+
this.acquireTimeout = envAcquire ? parseInt(envAcquire) : 60000;
|
|
129
181
|
this.token = token;
|
|
130
182
|
this.url = url;
|
|
183
|
+
this.origin = origin;
|
|
184
|
+
this.refreshDetails = refreshDetails;
|
|
131
185
|
const logPrefix = "[Pool]";
|
|
132
186
|
this.logger = {
|
|
133
187
|
log: (...args) => console.log(logPrefix, args),
|
|
@@ -146,7 +200,22 @@ export class WebsocketPool {
|
|
|
146
200
|
}
|
|
147
201
|
}
|
|
148
202
|
getWork() {
|
|
149
|
-
return this.queue.shift();
|
|
203
|
+
return this.queue.shift()?.task;
|
|
204
|
+
}
|
|
205
|
+
allWorkersDone() {
|
|
206
|
+
return this.workers.length > 0 && this.workers.every((worker) => worker.done);
|
|
207
|
+
}
|
|
208
|
+
// Called by a worker when it gives up permanently. If no workers remain,
|
|
209
|
+
// queued work can never run, so reject it instead of letting it hang.
|
|
210
|
+
onWorkerDone() {
|
|
211
|
+
if (!this.allWorkersDone()) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.logger.error("All workers are dead - draining queued work");
|
|
215
|
+
const error = new Error("WebsocketPool has no live workers");
|
|
216
|
+
while (this.queue.length > 0) {
|
|
217
|
+
this.queue.shift()?.reject(error);
|
|
218
|
+
}
|
|
150
219
|
}
|
|
151
220
|
async disconnect() {
|
|
152
221
|
this.logger.log("Disconnecting all workers...");
|
|
@@ -154,17 +223,47 @@ export class WebsocketPool {
|
|
|
154
223
|
this.logger.log("All workers disconnected");
|
|
155
224
|
}
|
|
156
225
|
async run(work) {
|
|
157
|
-
return new Promise(
|
|
158
|
-
this.
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
226
|
+
return new Promise((resolve, reject) => {
|
|
227
|
+
if (this.allWorkersDone()) {
|
|
228
|
+
reject(new Error("WebsocketPool has no live workers"));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
let settled = false;
|
|
232
|
+
const item = {
|
|
233
|
+
task: async (worker) => {
|
|
234
|
+
if (settled) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
settled = true;
|
|
238
|
+
clearTimeout(acquireTimer);
|
|
239
|
+
try {
|
|
240
|
+
const result = await work(worker);
|
|
241
|
+
resolve(result);
|
|
242
|
+
}
|
|
243
|
+
catch (e) {
|
|
244
|
+
worker.logger.error("Failed to run a job!");
|
|
245
|
+
reject(e);
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
reject: (reason) => {
|
|
249
|
+
if (settled) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
settled = true;
|
|
253
|
+
clearTimeout(acquireTimer);
|
|
254
|
+
reject(reason);
|
|
162
255
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
256
|
+
};
|
|
257
|
+
// Backstop so work can't sit in the queue forever if every worker
|
|
258
|
+
// is wedged. Worker death is handled separately by onWorkerDone.
|
|
259
|
+
const acquireTimer = setTimeout(() => {
|
|
260
|
+
const idx = this.queue.indexOf(item);
|
|
261
|
+
if (idx !== -1) {
|
|
262
|
+
this.queue.splice(idx, 1);
|
|
166
263
|
}
|
|
167
|
-
|
|
264
|
+
item.reject(new Error(`Timed out waiting for an available worker after ${this.acquireTimeout}ms`));
|
|
265
|
+
}, this.acquireTimeout);
|
|
266
|
+
this.queue.push(item);
|
|
168
267
|
});
|
|
169
268
|
}
|
|
170
269
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Options for the {@link WispWebSocket} adapter
|
|
3
|
+
*
|
|
4
|
+
* @param origin The value to send as the `Origin` header during the upgrade
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
export interface WsAdapterOptions {
|
|
9
|
+
origin?: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A thin adapter that exposes a Socket.IO-like event API over a plain `ws`
|
|
13
|
+
* WebSocket.
|
|
14
|
+
*
|
|
15
|
+
* The new backend speaks a simple JSON envelope rather than the Socket.IO /
|
|
16
|
+
* Engine.IO framing:
|
|
17
|
+
*
|
|
18
|
+
* ```json
|
|
19
|
+
* { "event": "console output", "args": ["...line..."] }
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* `args` is omitted entirely when an event carries no payload
|
|
23
|
+
* (e.g. `{ "event": "auth success" }`).
|
|
24
|
+
*
|
|
25
|
+
* This class translates between that envelope and the `.on/.once/.off/.emit`
|
|
26
|
+
* surface the rest of the codebase already uses, so the calling code barely
|
|
27
|
+
* changes.
|
|
28
|
+
*
|
|
29
|
+
* @remarks
|
|
30
|
+
* We *compose* an EventEmitter instead of extending one on purpose: extending
|
|
31
|
+
* it would mean overriding `emit()`, which EventEmitter also calls internally
|
|
32
|
+
* (e.g. the `newListener` event) — so every `.on()` would try to send a frame
|
|
33
|
+
* to the server. Composition keeps "emit to the wire" and "fire local
|
|
34
|
+
* listeners" cleanly separate.
|
|
35
|
+
*
|
|
36
|
+
* @internal
|
|
37
|
+
*/
|
|
38
|
+
export declare class WispWebSocket {
|
|
39
|
+
private url;
|
|
40
|
+
private origin?;
|
|
41
|
+
private ws?;
|
|
42
|
+
private emitter;
|
|
43
|
+
private outgoingListener?;
|
|
44
|
+
constructor(url: string, opts?: WsAdapterOptions);
|
|
45
|
+
/**
|
|
46
|
+
* Opens the underlying WebSocket and wires up frame translation.
|
|
47
|
+
*/
|
|
48
|
+
connect(): this;
|
|
49
|
+
/**
|
|
50
|
+
* Sends an event to the server as a `{ event, args }` JSON frame.
|
|
51
|
+
*
|
|
52
|
+
* @remarks
|
|
53
|
+
* Unlike EventEmitter.emit, this does NOT fire local listeners — it writes
|
|
54
|
+
* to the socket. Incoming frames fire local listeners via the internal
|
|
55
|
+
* emitter.
|
|
56
|
+
*/
|
|
57
|
+
emit(event: string, ...args: any[]): boolean;
|
|
58
|
+
on(event: string, listener: (...args: any[]) => void): this;
|
|
59
|
+
once(event: string, listener: (...args: any[]) => void): this;
|
|
60
|
+
/**
|
|
61
|
+
* Removes a specific listener, or — matching Socket.IO's behaviour — all
|
|
62
|
+
* listeners for an event when no listener is given.
|
|
63
|
+
*/
|
|
64
|
+
off(event: string, listener?: (...args: any[]) => void): this;
|
|
65
|
+
removeAllListeners(event?: string): this;
|
|
66
|
+
/**
|
|
67
|
+
* Registers a callback invoked for every outgoing emit (parity with
|
|
68
|
+
* Socket.IO's `onAnyOutgoing`, used for logging).
|
|
69
|
+
*/
|
|
70
|
+
onAnyOutgoing(listener: (...args: any[]) => void): this;
|
|
71
|
+
disconnect(): this;
|
|
72
|
+
}
|