@tangle-network/sandbox-ui 0.12.0 → 0.14.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/{chunk-ESVYBDGA.js → chunk-AG7QDC2Q.js} +182 -2
- package/dist/globals.css +407 -403
- package/dist/hooks.d.ts +1 -1
- package/dist/hooks.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/styles.css +407 -403
- package/dist/terminal.js +39 -2
- package/dist/{use-sandbox-metrics-B64diPqN.d.ts → use-sandbox-metrics-DWc0k9Xm.d.ts} +18 -6
- package/package.json +8 -1
|
@@ -3,6 +3,38 @@ import { useState, useEffect, useRef, useCallback } from "react";
|
|
|
3
3
|
function createEmptyBatch() {
|
|
4
4
|
return { data: "", waiters: [] };
|
|
5
5
|
}
|
|
6
|
+
var WS_OPEN_TIMEOUT_MS = 1500;
|
|
7
|
+
function toWsUrl(apiUrl, sessionId) {
|
|
8
|
+
try {
|
|
9
|
+
const url = new URL(`${apiUrl}/terminals/${sessionId}/ws`);
|
|
10
|
+
if (url.protocol === "https:") url.protocol = "wss:";
|
|
11
|
+
else if (url.protocol === "http:") url.protocol = "ws:";
|
|
12
|
+
else return null;
|
|
13
|
+
return url.toString();
|
|
14
|
+
} catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
var BEARER_SUBPROTOCOL_PREFIX = "bearer.";
|
|
19
|
+
function toBearerSubprotocol(token) {
|
|
20
|
+
if (typeof btoa === "undefined") return null;
|
|
21
|
+
let encoded;
|
|
22
|
+
try {
|
|
23
|
+
encoded = btoa(token);
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return `${BEARER_SUBPROTOCOL_PREFIX}${encoded.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")}`;
|
|
28
|
+
}
|
|
29
|
+
var stdinEncoder = typeof TextEncoder !== "undefined" ? new TextEncoder() : null;
|
|
30
|
+
function encodeStdin(text) {
|
|
31
|
+
if (!stdinEncoder) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
"TextEncoder is unavailable; WebSocket transport cannot encode stdin"
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
return stdinEncoder.encode(text);
|
|
37
|
+
}
|
|
6
38
|
function usePtySession({ apiUrl, token, onData }) {
|
|
7
39
|
const [isConnected, setIsConnected] = useState(false);
|
|
8
40
|
const [error, setError] = useState(null);
|
|
@@ -13,6 +45,8 @@ function usePtySession({ apiUrl, token, onData }) {
|
|
|
13
45
|
const mountedRef = useRef(true);
|
|
14
46
|
const onDataRef = useRef(onData);
|
|
15
47
|
const connectStreamRef = useRef(null);
|
|
48
|
+
const wsRef = useRef(null);
|
|
49
|
+
const pendingWsRef = useRef(null);
|
|
16
50
|
const pendingBatchRef = useRef(createEmptyBatch());
|
|
17
51
|
const drainPromiseRef = useRef(null);
|
|
18
52
|
const inputAbortRef = useRef(null);
|
|
@@ -37,8 +71,27 @@ function usePtySession({ apiUrl, token, onData }) {
|
|
|
37
71
|
abortRef.current = null;
|
|
38
72
|
}
|
|
39
73
|
}, []);
|
|
74
|
+
const closeWs = useCallback(() => {
|
|
75
|
+
const active = wsRef.current;
|
|
76
|
+
if (active) {
|
|
77
|
+
wsRef.current = null;
|
|
78
|
+
try {
|
|
79
|
+
active.close();
|
|
80
|
+
} catch {
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const pending = pendingWsRef.current;
|
|
84
|
+
if (pending) {
|
|
85
|
+
pendingWsRef.current = null;
|
|
86
|
+
try {
|
|
87
|
+
pending.close();
|
|
88
|
+
} catch {
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}, []);
|
|
40
92
|
const cleanup = useCallback(() => {
|
|
41
93
|
abortStream();
|
|
94
|
+
closeWs();
|
|
42
95
|
if (inputAbortRef.current) {
|
|
43
96
|
inputAbortRef.current.abort();
|
|
44
97
|
inputAbortRef.current = null;
|
|
@@ -55,7 +108,110 @@ function usePtySession({ apiUrl, token, onData }) {
|
|
|
55
108
|
}
|
|
56
109
|
rejectPendingInput("Terminal session is not connected");
|
|
57
110
|
setIsConnected(false);
|
|
58
|
-
}, [apiUrl, token, abortStream, rejectPendingInput]);
|
|
111
|
+
}, [apiUrl, token, abortStream, closeWs, rejectPendingInput]);
|
|
112
|
+
const connectWs = useCallback((sessionId) => {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
if (typeof WebSocket === "undefined" || stdinEncoder === null) {
|
|
115
|
+
resolve(false);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const wsUrl = toWsUrl(apiUrl, sessionId);
|
|
119
|
+
if (!wsUrl) {
|
|
120
|
+
resolve(false);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const subprotocol = toBearerSubprotocol(token);
|
|
124
|
+
if (!subprotocol) {
|
|
125
|
+
resolve(false);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
let ws;
|
|
129
|
+
try {
|
|
130
|
+
ws = new WebSocket(wsUrl, [subprotocol]);
|
|
131
|
+
} catch {
|
|
132
|
+
resolve(false);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
ws.binaryType = "arraybuffer";
|
|
136
|
+
pendingWsRef.current = ws;
|
|
137
|
+
let opened = false;
|
|
138
|
+
let settled = false;
|
|
139
|
+
const settle = (ok) => {
|
|
140
|
+
if (settled) return;
|
|
141
|
+
settled = true;
|
|
142
|
+
clearTimeout(handshakeTimer);
|
|
143
|
+
resolve(ok);
|
|
144
|
+
};
|
|
145
|
+
const handshakeTimer = setTimeout(() => {
|
|
146
|
+
if (opened) return;
|
|
147
|
+
try {
|
|
148
|
+
ws.close();
|
|
149
|
+
} catch {
|
|
150
|
+
}
|
|
151
|
+
settle(false);
|
|
152
|
+
}, WS_OPEN_TIMEOUT_MS);
|
|
153
|
+
ws.onopen = () => {
|
|
154
|
+
opened = true;
|
|
155
|
+
if (pendingWsRef.current !== ws || !mountedRef.current) {
|
|
156
|
+
try {
|
|
157
|
+
ws.close();
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
settle(false);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
pendingWsRef.current = null;
|
|
164
|
+
wsRef.current = ws;
|
|
165
|
+
setIsConnected(true);
|
|
166
|
+
setError(null);
|
|
167
|
+
retryCountRef.current = 0;
|
|
168
|
+
settle(true);
|
|
169
|
+
};
|
|
170
|
+
ws.onmessage = (ev) => {
|
|
171
|
+
if (!mountedRef.current) return;
|
|
172
|
+
const data = ev.data;
|
|
173
|
+
let text;
|
|
174
|
+
if (typeof data === "string") {
|
|
175
|
+
text = data;
|
|
176
|
+
} else if (data instanceof ArrayBuffer) {
|
|
177
|
+
text = new TextDecoder().decode(data);
|
|
178
|
+
} else if (ArrayBuffer.isView(data)) {
|
|
179
|
+
text = new TextDecoder().decode(data);
|
|
180
|
+
} else {
|
|
181
|
+
data.text().then((t) => {
|
|
182
|
+
if (mountedRef.current) onDataRef.current(t);
|
|
183
|
+
}).catch(() => {
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (text) onDataRef.current(text);
|
|
188
|
+
};
|
|
189
|
+
ws.onerror = () => {
|
|
190
|
+
};
|
|
191
|
+
ws.onclose = () => {
|
|
192
|
+
if (pendingWsRef.current === ws) {
|
|
193
|
+
pendingWsRef.current = null;
|
|
194
|
+
}
|
|
195
|
+
const wasActive = wsRef.current === ws;
|
|
196
|
+
if (wasActive) {
|
|
197
|
+
wsRef.current = null;
|
|
198
|
+
}
|
|
199
|
+
if (!opened) {
|
|
200
|
+
settle(false);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (!wasActive || !mountedRef.current) return;
|
|
204
|
+
setIsConnected(false);
|
|
205
|
+
if (sessionIdRef.current) {
|
|
206
|
+
retryTimerRef.current = setTimeout(() => {
|
|
207
|
+
if (mountedRef.current && sessionIdRef.current) {
|
|
208
|
+
connectStreamRef.current?.(sessionIdRef.current);
|
|
209
|
+
}
|
|
210
|
+
}, 1e3);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
});
|
|
214
|
+
}, [apiUrl, token]);
|
|
59
215
|
const connectStream = useCallback(async (sessionId) => {
|
|
60
216
|
abortStream();
|
|
61
217
|
setError(null);
|
|
@@ -162,7 +318,12 @@ function usePtySession({ apiUrl, token, onData }) {
|
|
|
162
318
|
if (!mountedRef.current) return;
|
|
163
319
|
sessionIdRef.current = sessionId;
|
|
164
320
|
inputAbortRef.current = new AbortController();
|
|
321
|
+
const wsOk = await connectWs(sessionId);
|
|
322
|
+
if (!mountedRef.current || sessionIdRef.current !== sessionId) return;
|
|
165
323
|
ensureDrainRunningRef.current?.();
|
|
324
|
+
if (wsOk) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
166
327
|
await connectStream(sessionId);
|
|
167
328
|
} catch (err) {
|
|
168
329
|
if (err.name === "AbortError") return;
|
|
@@ -172,10 +333,19 @@ function usePtySession({ apiUrl, token, onData }) {
|
|
|
172
333
|
setIsConnected(false);
|
|
173
334
|
}
|
|
174
335
|
}
|
|
175
|
-
}, [apiUrl, token, cleanup, connectStream]);
|
|
336
|
+
}, [apiUrl, token, cleanup, connectWs, connectStream]);
|
|
176
337
|
const resizeTerminal = useCallback(async (cols, rows) => {
|
|
177
338
|
const sid = sessionIdRef.current;
|
|
178
339
|
if (!sid || cols <= 0 || rows <= 0) return;
|
|
340
|
+
const ws = wsRef.current;
|
|
341
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
342
|
+
try {
|
|
343
|
+
ws.send(JSON.stringify({ type: "resize", cols, rows }));
|
|
344
|
+
return;
|
|
345
|
+
} catch (err) {
|
|
346
|
+
console.warn("Terminal resize over WS failed; falling back to HTTP", err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
179
349
|
try {
|
|
180
350
|
const res = await fetch(`${apiUrl}/terminals/${sid}`, {
|
|
181
351
|
method: "PATCH",
|
|
@@ -201,6 +371,16 @@ function usePtySession({ apiUrl, token, onData }) {
|
|
|
201
371
|
}
|
|
202
372
|
const batch = pendingBatchRef.current;
|
|
203
373
|
pendingBatchRef.current = createEmptyBatch();
|
|
374
|
+
const ws = wsRef.current;
|
|
375
|
+
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
376
|
+
try {
|
|
377
|
+
ws.send(encodeStdin(batch.data));
|
|
378
|
+
for (const w of batch.waiters) w.resolve();
|
|
379
|
+
} catch (err) {
|
|
380
|
+
for (const w of batch.waiters) w.reject(err);
|
|
381
|
+
}
|
|
382
|
+
continue;
|
|
383
|
+
}
|
|
204
384
|
try {
|
|
205
385
|
const res = await fetch(`${apiUrl}/terminals/${sid}/input`, {
|
|
206
386
|
method: "POST",
|