@aitty/server 0.1.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/dist/cli-token.d.ts +5 -0
- package/dist/cli-token.js +14 -0
- package/dist/frontend/browser-shell.d.ts +34 -0
- package/dist/frontend/browser-shell.html +218 -0
- package/dist/frontend/browser-shell.js +291 -0
- package/dist/frontend/terminal-app.js +4780 -0
- package/dist/frontend/terminal.css +268 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/logging.d.ts +17 -0
- package/dist/logging.js +34 -0
- package/dist/network-policy.d.ts +10 -0
- package/dist/network-policy.js +88 -0
- package/dist/runtime/dependencies.d.ts +16 -0
- package/dist/runtime/dependencies.js +16 -0
- package/dist/runtime/output-buffer.d.ts +11 -0
- package/dist/runtime/output-buffer.js +202 -0
- package/dist/runtime/pty-session.d.ts +59 -0
- package/dist/runtime/pty-session.js +247 -0
- package/dist/runtime/websocket-transport.d.ts +32 -0
- package/dist/runtime/websocket-transport.js +465 -0
- package/dist/server.d.ts +51 -0
- package/dist/server.js +234 -0
- package/dist/theme-source.d.ts +2 -0
- package/dist/theme-source.js +2 -0
- package/package.json +73 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
import { isSessionTokenAuthorized } from "../cli-token.js";
|
|
2
|
+
import { isHostAllowed, isOriginAllowed } from "../network-policy.js";
|
|
3
|
+
import { createStructuredLogger } from "../logging.js";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import { createExitControlFrame, createHelloControlFrame, createPingControlFrame, createPongControlFrame, createThemeControlFrame, isAittyResizeControlFrame, parseAittyControlFrame, subscribeThemeSource } from "@aitty/protocol";
|
|
6
|
+
//#region src/runtime/websocket-transport.ts
|
|
7
|
+
const DEFAULT_MAX_BUFFERED_AMOUNT_BYTES = 256 * 1024;
|
|
8
|
+
const DEFAULT_MAX_QUEUED_BYTES = 256 * 1024;
|
|
9
|
+
const DEFAULT_DRAIN_WAIT_MS = 250;
|
|
10
|
+
const DEFAULT_PRIORITY_OUTPUT_RECOVERY_MS = 1e3;
|
|
11
|
+
const DEFAULT_KEEPALIVE_INITIAL_DELAY_MS = 100;
|
|
12
|
+
const DEFAULT_KEEPALIVE_INTERVAL_MS = 3e4;
|
|
13
|
+
const SOCKET_OPEN = 1;
|
|
14
|
+
const SOCKET_CLOSED = 3;
|
|
15
|
+
const NORMAL_CLOSE_CODE = 1e3;
|
|
16
|
+
const SUPERSEDED_CLOSE_CODE = 4001;
|
|
17
|
+
const SUPERSEDED_CLOSE_REASON = "superseded";
|
|
18
|
+
const BACKPRESSURE_CLOSE_CODE = 4002;
|
|
19
|
+
const BACKPRESSURE_CLOSE_REASON = "backpressure";
|
|
20
|
+
function createWebSocketTransport(options) {
|
|
21
|
+
const server = options.server;
|
|
22
|
+
const websocketServer = new options.WebSocketServer({ noServer: true });
|
|
23
|
+
const logger = createStructuredLogger({
|
|
24
|
+
token: options.token,
|
|
25
|
+
verbose: options.verbose,
|
|
26
|
+
writer: options.stderr
|
|
27
|
+
});
|
|
28
|
+
const clients = /* @__PURE__ */ new Map();
|
|
29
|
+
let inputWriteChain = Promise.resolve();
|
|
30
|
+
const maxBufferedAmountBytes = options.maxBufferedAmountBytes ?? DEFAULT_MAX_BUFFERED_AMOUNT_BYTES;
|
|
31
|
+
const maxQueuedBytes = options.maxQueuedBytes ?? DEFAULT_MAX_QUEUED_BYTES;
|
|
32
|
+
const drainWaitMs = options.drainWaitMs ?? DEFAULT_DRAIN_WAIT_MS;
|
|
33
|
+
const keepaliveInitialDelayMs = Math.max(0, options.keepaliveInitialDelayMs ?? DEFAULT_KEEPALIVE_INITIAL_DELAY_MS);
|
|
34
|
+
const keepaliveIntervalMs = Math.max(0, options.keepaliveIntervalMs ?? DEFAULT_KEEPALIVE_INTERVAL_MS);
|
|
35
|
+
let closed = false;
|
|
36
|
+
let exitNotified = false;
|
|
37
|
+
let latestThemeFrame = null;
|
|
38
|
+
const themeSubscription = subscribeThemeSource(options.themeSource, (theme) => {
|
|
39
|
+
latestThemeFrame = createThemeControlFrame(theme);
|
|
40
|
+
for (const [websocket, clientState] of clients) {
|
|
41
|
+
if (!clientState.active || websocket.readyState !== SOCKET_OPEN) continue;
|
|
42
|
+
enqueueClientFrame(websocket, clientState, latestThemeFrame, false, {
|
|
43
|
+
drainWaitMs,
|
|
44
|
+
maxBufferedAmountBytes,
|
|
45
|
+
maxQueuedBytes,
|
|
46
|
+
logger
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
const sessionSubscription = options.session.onData((chunk) => {
|
|
51
|
+
const normalizedChunk = Buffer.from(chunk);
|
|
52
|
+
const activeClient = getActiveClient(clients);
|
|
53
|
+
if (!activeClient) return;
|
|
54
|
+
const [websocket, clientState] = activeClient;
|
|
55
|
+
if (websocket.readyState !== SOCKET_OPEN) return;
|
|
56
|
+
enqueueClientFrame(websocket, clientState, normalizedChunk, true, {
|
|
57
|
+
drainWaitMs,
|
|
58
|
+
maxBufferedAmountBytes,
|
|
59
|
+
maxQueuedBytes,
|
|
60
|
+
logger
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
const onUpgrade = (request, socket, head) => {
|
|
64
|
+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
65
|
+
if (requestUrl.pathname !== "/ws") {
|
|
66
|
+
rejectUpgrade(socket, 404, "Not Found");
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (options.networkPolicy && !isHostAllowed(request.headers.host, options.networkPolicy)) {
|
|
70
|
+
rejectUpgrade(socket, 403, "Forbidden");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!isSessionTokenAuthorized(options.token, requestUrl.searchParams.get("t"))) {
|
|
74
|
+
rejectUpgrade(socket, 403, "Forbidden");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (options.networkPolicy && !isOriginAllowed(request.headers.origin, options.networkPolicy)) {
|
|
78
|
+
rejectUpgrade(socket, 403, "Forbidden");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
websocketServer.handleUpgrade(request, socket, head, (websocket) => {
|
|
82
|
+
websocketServer.emit("connection", websocket, request);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
server.on("upgrade", onUpgrade);
|
|
86
|
+
websocketServer.on("connection", (websocket) => {
|
|
87
|
+
if (closed) {
|
|
88
|
+
websocket.close();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
logger.info("WebSocket client connected");
|
|
92
|
+
const clientState = {
|
|
93
|
+
active: true,
|
|
94
|
+
keepaliveTimer: null,
|
|
95
|
+
oversizedAllowanceBytes: 0,
|
|
96
|
+
pendingBytes: 0,
|
|
97
|
+
pendingFrames: [],
|
|
98
|
+
priorityModeUntil: 0,
|
|
99
|
+
sending: false
|
|
100
|
+
};
|
|
101
|
+
const previousClient = getActiveClient(clients);
|
|
102
|
+
if (previousClient) previousClient[1].active = false;
|
|
103
|
+
clients.set(websocket, clientState);
|
|
104
|
+
if (previousClient) supersedeClient(previousClient[0]);
|
|
105
|
+
const dispose = once(() => {
|
|
106
|
+
clientState.active = false;
|
|
107
|
+
clearKeepaliveTimer(clientState);
|
|
108
|
+
clientState.oversizedAllowanceBytes = 0;
|
|
109
|
+
clientState.pendingBytes = 0;
|
|
110
|
+
clientState.pendingFrames.length = 0;
|
|
111
|
+
clients.delete(websocket);
|
|
112
|
+
});
|
|
113
|
+
const replay = options.session.getBufferedOutput();
|
|
114
|
+
enqueueClientFrame(websocket, clientState, createHelloFrame(options.session, replay), false, {
|
|
115
|
+
allowOversizedFrame: true,
|
|
116
|
+
drainWaitMs,
|
|
117
|
+
maxBufferedAmountBytes,
|
|
118
|
+
maxQueuedBytes,
|
|
119
|
+
logger
|
|
120
|
+
});
|
|
121
|
+
if (latestThemeFrame) enqueueClientFrame(websocket, clientState, latestThemeFrame, false, {
|
|
122
|
+
drainWaitMs,
|
|
123
|
+
maxBufferedAmountBytes,
|
|
124
|
+
maxQueuedBytes,
|
|
125
|
+
logger
|
|
126
|
+
});
|
|
127
|
+
scheduleKeepaliveFrame(websocket, clientState, {
|
|
128
|
+
drainWaitMs,
|
|
129
|
+
keepaliveIntervalMs,
|
|
130
|
+
maxBufferedAmountBytes,
|
|
131
|
+
maxQueuedBytes,
|
|
132
|
+
logger
|
|
133
|
+
}, keepaliveInitialDelayMs);
|
|
134
|
+
websocket.on("close", (code, reason) => {
|
|
135
|
+
const closeReason = Buffer.from(reason).toString("utf8");
|
|
136
|
+
logger.info(`WebSocket client disconnected code=${code}${closeReason ? ` reason=${closeReason}` : ""}`);
|
|
137
|
+
dispose();
|
|
138
|
+
});
|
|
139
|
+
websocket.on("error", (error) => {
|
|
140
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
141
|
+
logger.warn(`WebSocket client error: ${message}`);
|
|
142
|
+
dispose();
|
|
143
|
+
});
|
|
144
|
+
websocket.on("message", (data, isBinary) => {
|
|
145
|
+
if (!clients.get(websocket)?.active) return;
|
|
146
|
+
if (isBinary) {
|
|
147
|
+
const payload = toBuffer(data);
|
|
148
|
+
scheduleKeepaliveFrame(websocket, clientState, {
|
|
149
|
+
drainWaitMs,
|
|
150
|
+
keepaliveIntervalMs,
|
|
151
|
+
maxBufferedAmountBytes,
|
|
152
|
+
maxQueuedBytes,
|
|
153
|
+
logger
|
|
154
|
+
}, keepaliveIntervalMs);
|
|
155
|
+
if (containsPriorityControlByte(payload)) prioritizeClientOutput(clientState);
|
|
156
|
+
inputWriteChain = inputWriteChain.catch(() => void 0).then(() => options.session.write(payload));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
handleControlFrame(websocket, data, options.session, logger);
|
|
160
|
+
scheduleKeepaliveFrame(websocket, clientState, {
|
|
161
|
+
drainWaitMs,
|
|
162
|
+
keepaliveIntervalMs,
|
|
163
|
+
maxBufferedAmountBytes,
|
|
164
|
+
maxQueuedBytes,
|
|
165
|
+
logger
|
|
166
|
+
}, keepaliveIntervalMs);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
return {
|
|
170
|
+
async close() {
|
|
171
|
+
if (closed) return;
|
|
172
|
+
closed = true;
|
|
173
|
+
server.removeListener("upgrade", onUpgrade);
|
|
174
|
+
sessionSubscription.dispose();
|
|
175
|
+
themeSubscription.dispose();
|
|
176
|
+
await Promise.all(Array.from(clients.keys(), (client) => shutdownClient(client)));
|
|
177
|
+
await closeWebSocketServer(websocketServer);
|
|
178
|
+
},
|
|
179
|
+
async notifyExit(event) {
|
|
180
|
+
if (closed || exitNotified) return;
|
|
181
|
+
exitNotified = true;
|
|
182
|
+
const payload = createExitControlFrame({
|
|
183
|
+
code: event.signal ? null : event.exitCode,
|
|
184
|
+
signal: resolveSignalName(event.signal)
|
|
185
|
+
});
|
|
186
|
+
await Promise.all(Array.from(clients.entries(), ([client, state]) => {
|
|
187
|
+
if (!state.active) return Promise.resolve();
|
|
188
|
+
return sendExitAndClose(client, payload);
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function handleControlFrame(websocket, data, session, logger) {
|
|
194
|
+
const controlFrame = parseAittyControlFrame(toBuffer(data).toString("utf8"));
|
|
195
|
+
if (!controlFrame) {
|
|
196
|
+
logger.warn("Ignoring malformed WebSocket control frame");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (controlFrame.type === "ping") {
|
|
200
|
+
sendPongFrame(websocket);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (controlFrame.type === "pong") return;
|
|
204
|
+
if (controlFrame.type === "resize") {
|
|
205
|
+
handleResizeControlFrame(controlFrame, session, logger);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
logger.warn(`Ignoring unknown WebSocket control frame type: ${controlFrame.type}`);
|
|
209
|
+
}
|
|
210
|
+
async function sendPongFrame(websocket) {
|
|
211
|
+
if (websocket.readyState !== SOCKET_OPEN) return;
|
|
212
|
+
try {
|
|
213
|
+
await sendFrame(websocket, createPongControlFrame(), false);
|
|
214
|
+
} catch {}
|
|
215
|
+
}
|
|
216
|
+
function handleResizeControlFrame(frame, session, logger) {
|
|
217
|
+
if (!isAittyResizeControlFrame(frame)) {
|
|
218
|
+
logger.warn("Ignoring invalid WebSocket resize frame");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (frame.cols === session.cols && frame.rows === session.rows) return;
|
|
222
|
+
session.resize(frame.cols, frame.rows);
|
|
223
|
+
}
|
|
224
|
+
function rejectUpgrade(socket, statusCode, statusText) {
|
|
225
|
+
const body = statusText;
|
|
226
|
+
socket.write(`HTTP/1.1 ${statusCode} ${statusText}\r\nConnection: close\r
|
|
227
|
+
Content-Type: text/plain; charset=utf-8\r
|
|
228
|
+
Content-Length: ${Buffer.byteLength(body)}\r\n\r
|
|
229
|
+
` + body);
|
|
230
|
+
socket.destroy();
|
|
231
|
+
}
|
|
232
|
+
function toBuffer(data) {
|
|
233
|
+
if (typeof data === "string") return Buffer.from(data, "utf8");
|
|
234
|
+
if (Buffer.isBuffer(data)) return data;
|
|
235
|
+
if (Array.isArray(data)) return Buffer.concat(data.map((chunk) => toBuffer(chunk)));
|
|
236
|
+
return Buffer.from(data);
|
|
237
|
+
}
|
|
238
|
+
function createHelloFrame(session, replay) {
|
|
239
|
+
return createHelloControlFrame({
|
|
240
|
+
cols: session.cols,
|
|
241
|
+
replay: replay ? replay.toString("base64") : "",
|
|
242
|
+
rows: session.rows
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
function enqueueClientFrame(websocket, clientState, payload, binary, options) {
|
|
246
|
+
if (!clientState.active || websocket.readyState !== SOCKET_OPEN) return;
|
|
247
|
+
const frame = createOutboundFrame(payload, binary, options.allowOversizedFrame ?? false);
|
|
248
|
+
if (frame.allowOversized) clientState.oversizedAllowanceBytes = Math.max(clientState.oversizedAllowanceBytes, frame.byteLength);
|
|
249
|
+
const queuedLimit = Math.max(options.maxQueuedBytes, clientState.oversizedAllowanceBytes);
|
|
250
|
+
if (clientState.pendingBytes + frame.byteLength > queuedLimit) {
|
|
251
|
+
if (frame.allowOversized && clientState.pendingBytes === 0 && clientState.pendingFrames.length === 0) {
|
|
252
|
+
clientState.pendingFrames.push(frame);
|
|
253
|
+
clientState.pendingBytes += frame.byteLength;
|
|
254
|
+
pumpClientQueue(websocket, clientState, options.maxBufferedAmountBytes, options.drainWaitMs, options.logger);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
closeClientForBackpressure(websocket, clientState, options.logger);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
clientState.pendingFrames.push(frame);
|
|
261
|
+
clientState.pendingBytes += frame.byteLength;
|
|
262
|
+
pumpClientQueue(websocket, clientState, options.maxBufferedAmountBytes, options.drainWaitMs, options.logger);
|
|
263
|
+
}
|
|
264
|
+
function createOutboundFrame(payload, binary, allowOversized) {
|
|
265
|
+
return {
|
|
266
|
+
allowOversized,
|
|
267
|
+
binary,
|
|
268
|
+
byteLength: typeof payload === "string" ? Buffer.byteLength(payload) : payload.length,
|
|
269
|
+
payload
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
async function pumpClientQueue(websocket, clientState, maxBufferedAmountBytes, drainWaitMs, logger) {
|
|
273
|
+
if (clientState.sending) return;
|
|
274
|
+
clientState.sending = true;
|
|
275
|
+
try {
|
|
276
|
+
while (clientState.active && websocket.readyState === SOCKET_OPEN) {
|
|
277
|
+
const frame = clientState.pendingFrames[0];
|
|
278
|
+
if (!frame) {
|
|
279
|
+
if (websocket.bufferedAmount <= maxBufferedAmountBytes) clientState.oversizedAllowanceBytes = 0;
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
const bufferedLimit = Math.max(maxBufferedAmountBytes, clientState.oversizedAllowanceBytes);
|
|
283
|
+
if (websocket.bufferedAmount > bufferedLimit) {
|
|
284
|
+
await waitForSocketWritable(websocket, clientState.priorityModeUntil > Date.now() ? Math.max(drainWaitMs, DEFAULT_PRIORITY_OUTPUT_RECOVERY_MS) : drainWaitMs);
|
|
285
|
+
if (!clientState.active || websocket.readyState !== SOCKET_OPEN) return;
|
|
286
|
+
if (websocket.bufferedAmount > bufferedLimit) {
|
|
287
|
+
if (clientState.priorityModeUntil > Date.now()) continue;
|
|
288
|
+
await closeClientForBackpressure(websocket, clientState, logger);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
clientState.pendingFrames.shift();
|
|
293
|
+
clientState.pendingBytes -= frame.byteLength;
|
|
294
|
+
try {
|
|
295
|
+
await sendFrame(websocket, frame.payload, frame.binary);
|
|
296
|
+
if (clientState.pendingFrames.every((pendingFrame) => !pendingFrame.allowOversized) && websocket.bufferedAmount <= maxBufferedAmountBytes) clientState.oversizedAllowanceBytes = 0;
|
|
297
|
+
} catch {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
} finally {
|
|
302
|
+
clientState.sending = false;
|
|
303
|
+
if (clientState.active && websocket.readyState === SOCKET_OPEN && clientState.pendingFrames.length > 0) pumpClientQueue(websocket, clientState, maxBufferedAmountBytes, drainWaitMs, logger);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
function resolveSignalName(signal) {
|
|
307
|
+
if (!signal) return null;
|
|
308
|
+
for (const [name, value] of Object.entries(os.constants.signals)) if (value === signal) return name;
|
|
309
|
+
return `SIG${signal}`;
|
|
310
|
+
}
|
|
311
|
+
async function sendExitAndClose(websocket, payload) {
|
|
312
|
+
if (websocket.readyState === SOCKET_CLOSED) return;
|
|
313
|
+
if (websocket.readyState === SOCKET_OPEN) {
|
|
314
|
+
try {
|
|
315
|
+
await sendFrame(websocket, payload, false);
|
|
316
|
+
} catch {}
|
|
317
|
+
websocket.close(NORMAL_CLOSE_CODE);
|
|
318
|
+
}
|
|
319
|
+
if (!await waitForSocketClose(websocket)) {
|
|
320
|
+
websocket.terminate();
|
|
321
|
+
await waitForSocketClose(websocket);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
async function supersedeClient(websocket) {
|
|
325
|
+
if (websocket.readyState === SOCKET_CLOSED) return;
|
|
326
|
+
if (websocket.readyState === SOCKET_OPEN) websocket.close(SUPERSEDED_CLOSE_CODE, SUPERSEDED_CLOSE_REASON);
|
|
327
|
+
if (!await waitForSocketClose(websocket)) {
|
|
328
|
+
websocket.terminate();
|
|
329
|
+
await waitForSocketClose(websocket);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
async function closeClientForBackpressure(websocket, clientState, logger) {
|
|
333
|
+
if (!clientState.active) return;
|
|
334
|
+
clientState.active = false;
|
|
335
|
+
clientState.pendingBytes = 0;
|
|
336
|
+
clientState.pendingFrames.length = 0;
|
|
337
|
+
logger.warn("Closing slow WebSocket client after backpressure limit exceeded");
|
|
338
|
+
if (websocket.readyState === SOCKET_CLOSED) return;
|
|
339
|
+
if (websocket.readyState === SOCKET_OPEN) websocket.close(BACKPRESSURE_CLOSE_CODE, BACKPRESSURE_CLOSE_REASON);
|
|
340
|
+
else {
|
|
341
|
+
websocket.terminate();
|
|
342
|
+
await waitForSocketClose(websocket);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!await waitForSocketClose(websocket)) {
|
|
346
|
+
websocket.terminate();
|
|
347
|
+
await waitForSocketClose(websocket);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
async function shutdownClient(websocket) {
|
|
351
|
+
if (websocket.readyState === SOCKET_CLOSED) return;
|
|
352
|
+
websocket.terminate();
|
|
353
|
+
await waitForSocketClose(websocket);
|
|
354
|
+
}
|
|
355
|
+
function closeWebSocketServer(websocketServer) {
|
|
356
|
+
return new Promise((resolve, reject) => {
|
|
357
|
+
websocketServer.close((error) => {
|
|
358
|
+
if (error) {
|
|
359
|
+
reject(error);
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
resolve();
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
function sendFrame(websocket, payload, binary = Buffer.isBuffer(payload)) {
|
|
367
|
+
return new Promise((resolve, reject) => {
|
|
368
|
+
websocket.send(payload, { binary }, (error) => {
|
|
369
|
+
if (error) {
|
|
370
|
+
reject(error);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
resolve();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
function waitForSocketWritable(websocket, timeout) {
|
|
378
|
+
const socket = getUnderlyingSocket(websocket);
|
|
379
|
+
if (!socket || websocket.bufferedAmount === 0 && !socket.writableNeedDrain) return Promise.resolve();
|
|
380
|
+
return new Promise((resolve) => {
|
|
381
|
+
const finish = once(() => {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
socket.removeListener("drain", finish);
|
|
384
|
+
websocket.removeListener("close", finish);
|
|
385
|
+
websocket.removeListener("error", finish);
|
|
386
|
+
resolve();
|
|
387
|
+
});
|
|
388
|
+
const timer = setTimeout(() => {
|
|
389
|
+
finish();
|
|
390
|
+
}, timeout);
|
|
391
|
+
socket.once("drain", finish);
|
|
392
|
+
websocket.once("close", finish);
|
|
393
|
+
websocket.once("error", finish);
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function waitForSocketClose(websocket, timeout = 250) {
|
|
397
|
+
if (websocket.readyState === SOCKET_CLOSED) return Promise.resolve(true);
|
|
398
|
+
return new Promise((resolve) => {
|
|
399
|
+
const finish = once((closed) => {
|
|
400
|
+
clearTimeout(timer);
|
|
401
|
+
websocket.removeListener("close", onClose);
|
|
402
|
+
websocket.removeListener("error", onError);
|
|
403
|
+
resolve(closed);
|
|
404
|
+
});
|
|
405
|
+
const timer = setTimeout(() => {
|
|
406
|
+
finish(websocket.readyState === SOCKET_CLOSED);
|
|
407
|
+
}, timeout);
|
|
408
|
+
const onClose = () => {
|
|
409
|
+
finish(true);
|
|
410
|
+
};
|
|
411
|
+
const onError = () => {
|
|
412
|
+
finish(websocket.readyState === SOCKET_CLOSED);
|
|
413
|
+
};
|
|
414
|
+
websocket.once("close", onClose);
|
|
415
|
+
websocket.once("error", onError);
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
function getUnderlyingSocket(websocket) {
|
|
419
|
+
return websocket._socket ?? null;
|
|
420
|
+
}
|
|
421
|
+
function once(callback) {
|
|
422
|
+
let called = false;
|
|
423
|
+
return (...args) => {
|
|
424
|
+
if (called) return;
|
|
425
|
+
called = true;
|
|
426
|
+
callback(...args);
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
function clearKeepaliveTimer(clientState) {
|
|
430
|
+
if (clientState.keepaliveTimer === null) return;
|
|
431
|
+
clearTimeout(clientState.keepaliveTimer);
|
|
432
|
+
clientState.keepaliveTimer = null;
|
|
433
|
+
}
|
|
434
|
+
function scheduleKeepaliveFrame(websocket, clientState, options, delayMs) {
|
|
435
|
+
clearKeepaliveTimer(clientState);
|
|
436
|
+
if (options.keepaliveIntervalMs <= 0) return;
|
|
437
|
+
clientState.keepaliveTimer = setTimeout(() => {
|
|
438
|
+
clientState.keepaliveTimer = null;
|
|
439
|
+
if (!clientState.active || websocket.readyState !== SOCKET_OPEN) return;
|
|
440
|
+
enqueueClientFrame(websocket, clientState, createPingControlFrame(), false, {
|
|
441
|
+
drainWaitMs: options.drainWaitMs,
|
|
442
|
+
maxBufferedAmountBytes: options.maxBufferedAmountBytes,
|
|
443
|
+
maxQueuedBytes: options.maxQueuedBytes,
|
|
444
|
+
logger: options.logger
|
|
445
|
+
});
|
|
446
|
+
scheduleKeepaliveFrame(websocket, clientState, options, options.keepaliveIntervalMs);
|
|
447
|
+
}, Math.max(0, delayMs));
|
|
448
|
+
}
|
|
449
|
+
function getActiveClient(clients) {
|
|
450
|
+
for (const entry of clients.entries()) if (entry[1].active) return entry;
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
function containsPriorityControlByte(payload) {
|
|
454
|
+
return payload.includes(3) || payload.includes(12) || isStandaloneEscapeByte(payload);
|
|
455
|
+
}
|
|
456
|
+
function isStandaloneEscapeByte(payload) {
|
|
457
|
+
return payload.length === 1 && payload[0] === 27;
|
|
458
|
+
}
|
|
459
|
+
function prioritizeClientOutput(clientState) {
|
|
460
|
+
clientState.pendingBytes = 0;
|
|
461
|
+
clientState.pendingFrames.length = 0;
|
|
462
|
+
clientState.priorityModeUntil = Date.now() + DEFAULT_PRIORITY_OUTPUT_RECOVERY_MS;
|
|
463
|
+
}
|
|
464
|
+
//#endregion
|
|
465
|
+
export { createWebSocketTransport };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { BrowserRuntimeKind, BrowserShellOptions } from "./frontend/browser-shell.js";
|
|
2
|
+
import { PtyModule, PtySession } from "./runtime/pty-session.js";
|
|
3
|
+
import { AittyThemeSource as AittyThemeSource$1 } from "./theme-source.js";
|
|
4
|
+
import { AittyPortlessIdentity, AittyPortlessIdentity as AittyPortlessIdentity$1, AittyPortlessOptions, AittyPortlessOptions as AittyPortlessOptions$1, AittyTheme } from "@aitty/protocol";
|
|
5
|
+
import * as _$ws from "ws";
|
|
6
|
+
|
|
7
|
+
//#region src/server.d.ts
|
|
8
|
+
interface AittyRuntimeDependencies {
|
|
9
|
+
WebSocketServer?: typeof _$ws.WebSocketServer;
|
|
10
|
+
nodePty: PtyModule;
|
|
11
|
+
}
|
|
12
|
+
interface AittyServerOptions {
|
|
13
|
+
args?: string[];
|
|
14
|
+
bufferSize?: number;
|
|
15
|
+
command: string;
|
|
16
|
+
cwd: string;
|
|
17
|
+
env?: NodeJS.ProcessEnv;
|
|
18
|
+
host?: string;
|
|
19
|
+
port?: number;
|
|
20
|
+
portless?: AittyPortlessOptions$1;
|
|
21
|
+
publicHost?: string;
|
|
22
|
+
publicOrigin?: string;
|
|
23
|
+
runtimeKind?: BrowserRuntimeKind;
|
|
24
|
+
shell?: BrowserShellOptions;
|
|
25
|
+
theme?: AittyTheme;
|
|
26
|
+
themeSource?: AittyThemeSource$1;
|
|
27
|
+
verbose?: boolean;
|
|
28
|
+
}
|
|
29
|
+
interface AittyRunningServer {
|
|
30
|
+
childPid: number;
|
|
31
|
+
close(signal?: NodeJS.Signals): Promise<number>;
|
|
32
|
+
closed: Promise<number>;
|
|
33
|
+
host: string;
|
|
34
|
+
port: number;
|
|
35
|
+
portless: AittyPortlessIdentity$1;
|
|
36
|
+
session: PtySession;
|
|
37
|
+
token: string;
|
|
38
|
+
url: string;
|
|
39
|
+
}
|
|
40
|
+
interface Writer {
|
|
41
|
+
write(chunk: string): boolean;
|
|
42
|
+
}
|
|
43
|
+
interface AittyServerIo {
|
|
44
|
+
registerSignalHandlers?: boolean;
|
|
45
|
+
stderr?: Writer;
|
|
46
|
+
}
|
|
47
|
+
declare function createAittyServer(options: AittyServerOptions, dependencies: AittyRuntimeDependencies, io?: AittyServerIo): Promise<AittyRunningServer>;
|
|
48
|
+
declare function buildAittyUrl(origin: string, token: string, pathname: string): string;
|
|
49
|
+
declare function buildAittyOrigin(host: string, port: number): string;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { type AittyPortlessIdentity, type AittyPortlessOptions, AittyRunningServer, AittyRuntimeDependencies, AittyServerIo, AittyServerOptions, Writer, buildAittyOrigin, buildAittyUrl, createAittyServer };
|