-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbrowserbox-client.js
More file actions
128 lines (117 loc) · 3.89 KB
/
browserbox-client.js
File metadata and controls
128 lines (117 loc) · 3.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/**
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* browserbox-client.js
* Client for the BrowserBox Demo Server API.
*/
export class BrowserBoxClient {
constructor(serverBaseUrl = '') {
this.baseUrl = serverBaseUrl;
}
/**
* Create a new BrowserBox session.
* @param {{clientIP?: string|null}=} options
* @returns {Promise<{loginUrl: string, region: string, remainingMs: number, sessionId: string}>}
*/
async createSession(options = {}) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 120000);
try {
const payload = {};
if (options.clientIP && typeof options.clientIP === 'string') {
payload.clientIP = options.clientIP;
}
const response = await fetch(`${this.baseUrl}/api/session`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
signal: controller.signal,
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.error || 'Failed to create session');
}
return data;
} finally {
clearTimeout(timeoutId);
}
}
/**
* Check status of the active session for current browser cookie.
*/
async checkStatus() {
const response = await fetch(`${this.baseUrl}/api/status`);
if (!response.ok) {
console.warn('Failed to check status', response.status);
return null;
}
return response.json();
}
/**
* Keep the remote session alive while client is connected.
* @param {string} sessionId
*/
async sendHeartbeat(sessionId) {
if (!sessionId) {
return;
}
try {
const response = await fetch(`${this.baseUrl}/api/session/heartbeat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sessionId }),
keepalive: true,
});
if (!response.ok) {
const isSessionExpired = (
response.status === 401 ||
response.status === 403 ||
response.status === 404 ||
response.status === 410
);
if (!isSessionExpired) {
return;
}
const error = new Error(`Heartbeat failed (${response.status})`);
error.status = response.status;
error.code = 'SESSION_EXPIRED';
throw error;
}
} catch (error) {
if (error?.code === 'SESSION_EXPIRED') {
throw error;
}
// Network/transient heartbeat failures are tolerated.
}
}
/**
* Notify server that the page is disconnecting.
* @param {string} sessionId
*/
async notifyDisconnect(sessionId) {
if (!sessionId) {
return;
}
const url = `${this.baseUrl}/api/session/disconnect`;
const payload = JSON.stringify({ sessionId });
if (typeof navigator !== 'undefined' && typeof navigator.sendBeacon === 'function') {
try {
const blob = new Blob([payload], { type: 'application/json' });
navigator.sendBeacon(url, blob);
return;
} catch {
// Fall back to fetch keepalive.
}
}
try {
await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: payload,
keepalive: true,
});
} catch {
// Disconnect failures are tolerated.
}
}
}