agent-yes 1.195.0 → 1.196.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/SUPPORTED_CLIS-Ca2ffDRy.js +8 -0
- package/dist/{SUPPORTED_CLIS-Bdj9qDRm.js → SUPPORTED_CLIS-CxLCQtAU.js} +2 -2
- package/dist/{agentShare-M2jaTMlH.js → agentShare-Bi95Xy0S.js} +2 -2
- package/dist/cli.js +4 -4
- package/dist/index.js +2 -2
- package/dist/{notifyDaemon-D5HwUMxH.js → notifyDaemon-BdOeFIVl.js} +2 -2
- package/dist/{rustBinary-B4pfibDJ.js → rustBinary-D_ifX2Vd.js} +2 -2
- package/dist/{schedule-1oS-Yxxa.js → schedule-rvMgc0tA.js} +4 -4
- package/dist/{serve-Ce708ALB.js → serve-D0-LoTfw.js} +10 -10
- package/dist/{setup-fTLT9Bf2.js → setup-BC1VjTOf.js} +2 -2
- package/dist/{subcommands-CV5gqRKq.js → subcommands-Cz8xpc5X.js} +1 -1
- package/dist/{subcommands-CqB9zo8_.js → subcommands-s74ViJcI.js} +6 -6
- package/dist/{ts-DDsICrj-.js → ts-CP17kUI1.js} +2 -2
- package/dist/{versionChecker-B8qEzYDu.js → versionChecker-DTssk7cU.js} +2 -2
- package/lab/ui/index.html +96 -0
- package/lab/ui/room-client.js +544 -388
- package/lab/ui/sw.js +71 -19
- package/package.json +1 -1
- package/dist/SUPPORTED_CLIS-Daig8XHX.js +0 -8
package/lab/ui/room-client.js
CHANGED
|
@@ -1,210 +1,258 @@
|
|
|
1
|
-
|
|
1
|
+
// src/shared/signaling.ts
|
|
2
|
+
var CLIENT_WIRE_ROLE = "viewer";
|
|
3
|
+
function newPeerId() {
|
|
2
4
|
return crypto.randomUUID();
|
|
3
5
|
}
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
|
|
7
|
+
// src/shared/signaling-client.ts
|
|
8
|
+
var STABLE_MS = 60000;
|
|
9
|
+
var CONNECT_TIMEOUT_MS = 1e4;
|
|
10
|
+
var RECONNECT_MIN_MS = 1000;
|
|
11
|
+
var RECONNECT_MAX_MS = 120000;
|
|
12
|
+
var HEARTBEAT_MS = 25000;
|
|
13
|
+
|
|
14
|
+
class SignalingClient {
|
|
10
15
|
opts;
|
|
11
16
|
peerId;
|
|
12
17
|
ws = null;
|
|
13
|
-
closed =
|
|
14
|
-
reconnectDelay =
|
|
18
|
+
closed = false;
|
|
19
|
+
reconnectDelay = RECONNECT_MIN_MS;
|
|
15
20
|
reconnectTimer = null;
|
|
16
|
-
dormant =
|
|
21
|
+
dormant = false;
|
|
17
22
|
heartbeat = null;
|
|
18
23
|
stableTimer = null;
|
|
19
24
|
openedAt = 0;
|
|
20
|
-
constructor(
|
|
21
|
-
this.opts =
|
|
22
|
-
this.peerId =
|
|
25
|
+
constructor(opts) {
|
|
26
|
+
this.opts = opts;
|
|
27
|
+
this.peerId = opts.peerId ?? newPeerId();
|
|
23
28
|
}
|
|
24
29
|
connect() {
|
|
25
|
-
|
|
30
|
+
this.closed = false;
|
|
31
|
+
this.attachWakeListeners();
|
|
32
|
+
this.open();
|
|
26
33
|
}
|
|
27
34
|
onWake = () => {
|
|
28
|
-
if (this.closed)
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (
|
|
35
|
+
if (this.closed)
|
|
36
|
+
return;
|
|
37
|
+
const state = this.ws?.readyState;
|
|
38
|
+
if (state === 1)
|
|
39
|
+
return;
|
|
40
|
+
if (state === 0) {
|
|
32
41
|
try {
|
|
33
42
|
this.ws?.close();
|
|
34
43
|
} catch {}
|
|
35
44
|
return;
|
|
36
45
|
}
|
|
37
|
-
if (this.dormant || this.reconnectTimer != null)
|
|
38
|
-
|
|
46
|
+
if (this.dormant || this.reconnectTimer != null) {
|
|
47
|
+
this.dormant = false;
|
|
48
|
+
this.clearReconnectTimer();
|
|
49
|
+
this.open();
|
|
50
|
+
}
|
|
39
51
|
};
|
|
40
52
|
hidden() {
|
|
41
|
-
|
|
53
|
+
const doc = globalThis.document;
|
|
54
|
+
return doc?.visibilityState === "hidden";
|
|
42
55
|
}
|
|
43
56
|
attachWakeListeners() {
|
|
44
|
-
globalThis.document
|
|
45
|
-
|
|
46
|
-
|
|
57
|
+
const doc = globalThis.document;
|
|
58
|
+
doc?.addEventListener("visibilitychange", this.onWake);
|
|
59
|
+
const win = globalThis.window;
|
|
60
|
+
win?.addEventListener("focus", this.onWake);
|
|
61
|
+
win?.addEventListener("online", this.onWake);
|
|
47
62
|
}
|
|
48
63
|
detachWakeListeners() {
|
|
49
|
-
globalThis.document
|
|
50
|
-
|
|
51
|
-
|
|
64
|
+
const doc = globalThis.document;
|
|
65
|
+
doc?.removeEventListener("visibilitychange", this.onWake);
|
|
66
|
+
const win = globalThis.window;
|
|
67
|
+
win?.removeEventListener("focus", this.onWake);
|
|
68
|
+
win?.removeEventListener("online", this.onWake);
|
|
52
69
|
}
|
|
53
70
|
roomUrl() {
|
|
54
|
-
|
|
71
|
+
const base = this.opts.url.replace(/\/+$/, "");
|
|
72
|
+
return `${base}/room/${encodeURIComponent(this.opts.token)}`;
|
|
55
73
|
}
|
|
56
74
|
open() {
|
|
57
|
-
|
|
58
|
-
this.ws =
|
|
59
|
-
|
|
60
|
-
if (
|
|
75
|
+
const ws = new WebSocket(this.roomUrl());
|
|
76
|
+
this.ws = ws;
|
|
77
|
+
const connectTimer = setTimeout(() => {
|
|
78
|
+
if (ws.readyState === 0) {
|
|
61
79
|
try {
|
|
62
|
-
|
|
80
|
+
ws.close();
|
|
63
81
|
} catch {}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
82
|
+
}
|
|
83
|
+
}, CONNECT_TIMEOUT_MS);
|
|
84
|
+
ws.onopen = () => {
|
|
85
|
+
clearTimeout(connectTimer);
|
|
86
|
+
this.openedAt = Date.now();
|
|
87
|
+
this.clearStableTimer();
|
|
88
|
+
this.stableTimer = setTimeout(() => {
|
|
89
|
+
this.reconnectDelay = RECONNECT_MIN_MS;
|
|
90
|
+
}, STABLE_MS);
|
|
91
|
+
const hello = {
|
|
73
92
|
type: "hello",
|
|
74
93
|
role: this.opts.role,
|
|
75
94
|
peerId: this.peerId,
|
|
76
|
-
...
|
|
95
|
+
...this.opts.meta ? { meta: this.opts.meta } : {}
|
|
77
96
|
};
|
|
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
|
-
|
|
97
|
+
ws.send(JSON.stringify(hello));
|
|
98
|
+
this.startHeartbeat();
|
|
99
|
+
this.opts.onOpen?.();
|
|
100
|
+
};
|
|
101
|
+
ws.onmessage = (ev) => {
|
|
102
|
+
let msg;
|
|
103
|
+
try {
|
|
104
|
+
msg = JSON.parse(String(ev.data));
|
|
105
|
+
} catch {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
if (msg.type === "peers")
|
|
109
|
+
this.opts.onPeers?.(msg.peers, msg.now);
|
|
110
|
+
else if (msg.type === "signal")
|
|
111
|
+
this.opts.onSignal?.(msg.from, msg.data);
|
|
112
|
+
};
|
|
113
|
+
ws.onclose = (ev) => {
|
|
114
|
+
clearTimeout(connectTimer);
|
|
115
|
+
this.clearStableTimer();
|
|
116
|
+
this.stopHeartbeat();
|
|
117
|
+
const ms = this.openedAt ? Date.now() - this.openedAt : 0;
|
|
118
|
+
this.openedAt = 0;
|
|
119
|
+
this.opts.onClose?.({ code: ev?.code ?? 0, reason: ev?.reason ?? "", ms });
|
|
120
|
+
if (!this.closed)
|
|
121
|
+
this.scheduleReconnect();
|
|
122
|
+
};
|
|
123
|
+
ws.onerror = () => {
|
|
124
|
+
try {
|
|
125
|
+
ws.close();
|
|
126
|
+
} catch {}
|
|
127
|
+
};
|
|
105
128
|
}
|
|
106
129
|
startHeartbeat() {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
130
|
+
this.stopHeartbeat();
|
|
131
|
+
this.heartbeat = setInterval(() => {
|
|
132
|
+
try {
|
|
133
|
+
this.ws?.send(JSON.stringify({ type: "ping" }));
|
|
134
|
+
} catch {}
|
|
135
|
+
}, HEARTBEAT_MS);
|
|
113
136
|
}
|
|
114
137
|
stopHeartbeat() {
|
|
115
|
-
if (this.heartbeat != null)
|
|
138
|
+
if (this.heartbeat != null) {
|
|
139
|
+
clearInterval(this.heartbeat);
|
|
140
|
+
this.heartbeat = null;
|
|
141
|
+
}
|
|
116
142
|
}
|
|
117
143
|
clearStableTimer() {
|
|
118
|
-
if (this.stableTimer != null)
|
|
144
|
+
if (this.stableTimer != null) {
|
|
145
|
+
clearTimeout(this.stableTimer);
|
|
146
|
+
this.stableTimer = null;
|
|
147
|
+
}
|
|
119
148
|
}
|
|
120
149
|
scheduleReconnect() {
|
|
121
150
|
if (this.hidden()) {
|
|
122
|
-
this.dormant =
|
|
151
|
+
this.dormant = true;
|
|
123
152
|
return;
|
|
124
153
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
}
|
|
154
|
+
const delay = Math.round(this.reconnectDelay * (0.75 + Math.random() * 0.5));
|
|
155
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
156
|
+
this.clearReconnectTimer();
|
|
157
|
+
this.reconnectTimer = setTimeout(() => {
|
|
158
|
+
this.reconnectTimer = null;
|
|
159
|
+
if (this.closed)
|
|
160
|
+
return;
|
|
161
|
+
if (this.hidden()) {
|
|
162
|
+
this.dormant = true;
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.open();
|
|
166
|
+
}, delay);
|
|
136
167
|
}
|
|
137
168
|
clearReconnectTimer() {
|
|
138
|
-
if (this.reconnectTimer != null)
|
|
139
|
-
|
|
169
|
+
if (this.reconnectTimer != null) {
|
|
170
|
+
clearTimeout(this.reconnectTimer);
|
|
171
|
+
this.reconnectTimer = null;
|
|
172
|
+
}
|
|
140
173
|
}
|
|
141
|
-
sendSignal(
|
|
142
|
-
|
|
143
|
-
this.ws?.send(JSON.stringify(
|
|
174
|
+
sendSignal(to, data) {
|
|
175
|
+
const msg = { type: "signal", to, data };
|
|
176
|
+
this.ws?.send(JSON.stringify(msg));
|
|
144
177
|
}
|
|
145
|
-
updateMeta(
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
178
|
+
updateMeta(meta) {
|
|
179
|
+
this.opts.meta = meta;
|
|
180
|
+
if (this.ws?.readyState === 1) {
|
|
181
|
+
const msg = { type: "meta", meta };
|
|
182
|
+
this.ws.send(JSON.stringify(msg));
|
|
149
183
|
}
|
|
150
184
|
}
|
|
151
185
|
close() {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
186
|
+
this.closed = true;
|
|
187
|
+
this.dormant = false;
|
|
188
|
+
this.detachWakeListeners();
|
|
189
|
+
this.clearReconnectTimer();
|
|
190
|
+
this.stopHeartbeat();
|
|
191
|
+
this.clearStableTimer();
|
|
158
192
|
try {
|
|
159
193
|
this.ws?.close();
|
|
160
194
|
} catch {}
|
|
161
195
|
}
|
|
162
196
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
197
|
+
|
|
198
|
+
// src/shared/rtc.ts
|
|
199
|
+
var ICE_SERVERS = [
|
|
200
|
+
"stun:stun.l.google.com:19302",
|
|
201
|
+
"stun:stun1.l.google.com:19302"
|
|
202
|
+
];
|
|
203
|
+
var CHANNEL_LABEL = "codehost";
|
|
204
|
+
var BULK_CHANNEL_LABEL = "codehost-bulk";
|
|
205
|
+
|
|
206
|
+
// src/web/rtc-client.ts
|
|
207
|
+
class RtcClient {
|
|
167
208
|
opts;
|
|
168
209
|
pc;
|
|
169
210
|
channel = null;
|
|
170
211
|
bulk = null;
|
|
171
|
-
constructor(
|
|
172
|
-
this.opts =
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
212
|
+
constructor(opts) {
|
|
213
|
+
this.opts = opts;
|
|
214
|
+
this.pc = new RTCPeerConnection({
|
|
215
|
+
iceServers: ICE_SERVERS.map((urls) => ({ urls }))
|
|
216
|
+
});
|
|
217
|
+
this.pc.onicecandidate = (ev) => {
|
|
218
|
+
if (ev.candidate) {
|
|
219
|
+
this.opts.sendSignal({
|
|
220
|
+
kind: "candidate",
|
|
221
|
+
candidate: ev.candidate.candidate,
|
|
222
|
+
mid: ev.candidate.sdpMid ?? "0"
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
this.pc.onconnectionstatechange = () => {
|
|
227
|
+
this.opts.onState?.(this.pc.connectionState);
|
|
228
|
+
};
|
|
185
229
|
}
|
|
186
230
|
async start() {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
231
|
+
const channel = this.pc.createDataChannel(CHANNEL_LABEL, { ordered: true });
|
|
232
|
+
channel.binaryType = "arraybuffer";
|
|
233
|
+
this.channel = channel;
|
|
234
|
+
channel.onopen = () => this.opts.onOpen?.(channel);
|
|
235
|
+
channel.onclose = () => this.opts.onClose?.();
|
|
236
|
+
const bulk = this.pc.createDataChannel(BULK_CHANNEL_LABEL, { ordered: true });
|
|
237
|
+
bulk.binaryType = "arraybuffer";
|
|
238
|
+
this.bulk = bulk;
|
|
239
|
+
const offer = await this.pc.createOffer();
|
|
240
|
+
await this.pc.setLocalDescription(offer);
|
|
241
|
+
this.opts.sendSignal({ kind: "offer", type: "offer", sdp: offer.sdp ?? "" });
|
|
242
|
+
}
|
|
243
|
+
async handleSignal(data) {
|
|
244
|
+
const sig = data;
|
|
245
|
+
if (!sig || typeof sig !== "object")
|
|
246
|
+
return;
|
|
247
|
+
if (sig.kind === "answer") {
|
|
248
|
+
await this.pc.setRemoteDescription({ type: "answer", sdp: sig.sdp });
|
|
249
|
+
} else if (sig.kind === "candidate") {
|
|
203
250
|
try {
|
|
204
|
-
await this.pc.addIceCandidate({ candidate:
|
|
205
|
-
} catch (
|
|
206
|
-
console.error("[rtc] addIceCandidate failed:",
|
|
251
|
+
await this.pc.addIceCandidate({ candidate: sig.candidate, sdpMid: sig.mid });
|
|
252
|
+
} catch (err) {
|
|
253
|
+
console.error("[rtc] addIceCandidate failed:", err);
|
|
207
254
|
}
|
|
255
|
+
}
|
|
208
256
|
}
|
|
209
257
|
get dataChannel() {
|
|
210
258
|
return this.channel;
|
|
@@ -214,35 +262,33 @@ class B {
|
|
|
214
262
|
}
|
|
215
263
|
async selectedPath() {
|
|
216
264
|
try {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (
|
|
265
|
+
const stats = await this.pc.getStats();
|
|
266
|
+
let pairId = null;
|
|
267
|
+
stats.forEach((s) => {
|
|
268
|
+
if (s.type === "transport" && s.selectedCandidatePairId)
|
|
269
|
+
pairId = s.selectedCandidatePairId;
|
|
221
270
|
});
|
|
222
|
-
let
|
|
223
|
-
|
|
224
|
-
(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}),
|
|
230
|
-
!Y)
|
|
231
|
-
)
|
|
271
|
+
let pair = null;
|
|
272
|
+
stats.forEach((s) => {
|
|
273
|
+
if (pairId ? s.id === pairId : s.type === "candidate-pair" && s.state === "succeeded" && s.nominated) {
|
|
274
|
+
pair = s;
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
if (!pair)
|
|
232
278
|
return null;
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
(
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
)
|
|
279
|
+
const { localCandidateId, remoteCandidateId } = pair;
|
|
280
|
+
let lan = true;
|
|
281
|
+
let found = 0;
|
|
282
|
+
stats.forEach((s) => {
|
|
283
|
+
if (s.id === localCandidateId || s.id === remoteCandidateId) {
|
|
284
|
+
found++;
|
|
285
|
+
if (s.candidateType !== "host")
|
|
286
|
+
lan = false;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
if (found < 2)
|
|
244
290
|
return null;
|
|
245
|
-
return
|
|
291
|
+
return lan ? "lan" : "p2p";
|
|
246
292
|
} catch {
|
|
247
293
|
return null;
|
|
248
294
|
}
|
|
@@ -259,280 +305,390 @@ class B {
|
|
|
259
305
|
} catch {}
|
|
260
306
|
}
|
|
261
307
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
308
|
+
|
|
309
|
+
// src/tunnel/protocol.ts
|
|
310
|
+
var FRAME_HEADER = 5;
|
|
311
|
+
var MAX_FRAME = 64 * 1024;
|
|
312
|
+
var MAX_CHUNK = MAX_FRAME - FRAME_HEADER;
|
|
313
|
+
var enc = new TextEncoder;
|
|
314
|
+
var dec = new TextDecoder;
|
|
315
|
+
function encodeFrame(op, streamId, payload) {
|
|
316
|
+
const len = payload?.byteLength ?? 0;
|
|
317
|
+
const buf = new Uint8Array(5 + len);
|
|
318
|
+
buf[0] = op;
|
|
319
|
+
new DataView(buf.buffer).setUint32(1, streamId >>> 0, false);
|
|
320
|
+
if (payload && len)
|
|
321
|
+
buf.set(payload, 5);
|
|
322
|
+
return buf;
|
|
272
323
|
}
|
|
273
|
-
function
|
|
274
|
-
|
|
275
|
-
Y = Q[0],
|
|
276
|
-
Z = new DataView(Q.buffer, Q.byteOffset, Q.byteLength).getUint32(1, !1),
|
|
277
|
-
$ = Q.subarray(5);
|
|
278
|
-
return { op: Y, streamId: Z, payload: $ };
|
|
324
|
+
function encodeJson(op, streamId, obj) {
|
|
325
|
+
return encodeFrame(op, streamId, enc.encode(JSON.stringify(obj)));
|
|
279
326
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
327
|
+
function decodeFrame(data) {
|
|
328
|
+
const u8 = data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
329
|
+
const op = u8[0];
|
|
330
|
+
const streamId = new DataView(u8.buffer, u8.byteOffset, u8.byteLength).getUint32(1, false);
|
|
331
|
+
const payload = u8.subarray(5);
|
|
332
|
+
return { op, streamId, payload };
|
|
282
333
|
}
|
|
283
|
-
function
|
|
284
|
-
return
|
|
334
|
+
function payloadJson(payload) {
|
|
335
|
+
return JSON.parse(dec.decode(payload));
|
|
285
336
|
}
|
|
286
|
-
function
|
|
287
|
-
|
|
337
|
+
function payloadText(payload) {
|
|
338
|
+
return dec.decode(payload);
|
|
288
339
|
}
|
|
289
|
-
function
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
Z = 0;
|
|
294
|
-
for (let $ of z) (Y.set($, Z), (Z += $.byteLength));
|
|
295
|
-
return Y;
|
|
340
|
+
function* chunk(body) {
|
|
341
|
+
for (let off = 0;off < body.byteLength; off += MAX_CHUNK) {
|
|
342
|
+
yield body.slice(off, Math.min(off + MAX_CHUNK, body.byteLength));
|
|
343
|
+
}
|
|
296
344
|
}
|
|
297
|
-
function
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
345
|
+
function concatBytes(parts) {
|
|
346
|
+
if (parts.length === 1)
|
|
347
|
+
return parts[0];
|
|
348
|
+
const total = parts.reduce((n, p) => n + p.byteLength, 0);
|
|
349
|
+
const out = new Uint8Array(total);
|
|
350
|
+
let off = 0;
|
|
351
|
+
for (const p of parts) {
|
|
352
|
+
out.set(p, off);
|
|
353
|
+
off += p.byteLength;
|
|
354
|
+
}
|
|
355
|
+
return out;
|
|
301
356
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
else this.pending.set(z, [Q.slice()]);
|
|
308
|
-
}
|
|
309
|
-
finish(z, Q) {
|
|
310
|
-
let Y = this.pending.get(z);
|
|
311
|
-
if (!Y) return Q;
|
|
312
|
-
return (this.pending.delete(z), Y.push(Q), f(Y));
|
|
357
|
+
function* wsMessageFrames(terminal, streamId, payload) {
|
|
358
|
+
let off = 0;
|
|
359
|
+
while (payload.byteLength - off > MAX_CHUNK) {
|
|
360
|
+
yield encodeFrame(13 /* WsCont */, streamId, payload.subarray(off, off + MAX_CHUNK));
|
|
361
|
+
off += MAX_CHUNK;
|
|
313
362
|
}
|
|
314
|
-
|
|
315
|
-
|
|
363
|
+
yield encodeFrame(terminal, streamId, payload.subarray(off));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
class WsReassembler {
|
|
367
|
+
pending = new Map;
|
|
368
|
+
cont(streamId, payload) {
|
|
369
|
+
const buf = this.pending.get(streamId);
|
|
370
|
+
if (buf)
|
|
371
|
+
buf.push(payload.slice());
|
|
372
|
+
else
|
|
373
|
+
this.pending.set(streamId, [payload.slice()]);
|
|
374
|
+
}
|
|
375
|
+
finish(streamId, payload) {
|
|
376
|
+
const buf = this.pending.get(streamId);
|
|
377
|
+
if (!buf)
|
|
378
|
+
return payload;
|
|
379
|
+
this.pending.delete(streamId);
|
|
380
|
+
buf.push(payload);
|
|
381
|
+
return concatBytes(buf);
|
|
382
|
+
}
|
|
383
|
+
drop(streamId) {
|
|
384
|
+
this.pending.delete(streamId);
|
|
316
385
|
}
|
|
317
386
|
}
|
|
318
|
-
|
|
319
|
-
|
|
387
|
+
|
|
388
|
+
// src/tunnel/client.ts
|
|
389
|
+
class TunnelClient {
|
|
390
|
+
transport;
|
|
320
391
|
bulk;
|
|
321
392
|
nextStreamId = 1;
|
|
322
|
-
https = new Map
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
this.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
)
|
|
334
|
-
|
|
393
|
+
https = new Map;
|
|
394
|
+
httpLane = new Map;
|
|
395
|
+
wss = new Map;
|
|
396
|
+
wsRx = new WsReassembler;
|
|
397
|
+
textEncoder = new TextEncoder;
|
|
398
|
+
constructor(transport, bulk = null) {
|
|
399
|
+
this.transport = transport;
|
|
400
|
+
this.bulk = bulk;
|
|
401
|
+
transport.onFrame((data) => this.onFrame(data));
|
|
402
|
+
bulk?.onFrame((data) => this.onFrame(data));
|
|
403
|
+
transport.onClose(() => this.failLane(transport, true));
|
|
404
|
+
bulk?.onClose(() => this.failLane(bulk, false));
|
|
405
|
+
}
|
|
406
|
+
failLane(lane, interactive) {
|
|
407
|
+
for (const [streamId, waiter] of [...this.https]) {
|
|
408
|
+
if ((this.httpLane.get(streamId) ?? this.transport) !== lane)
|
|
409
|
+
continue;
|
|
410
|
+
this.https.delete(streamId);
|
|
411
|
+
this.httpLane.delete(streamId);
|
|
412
|
+
waiter.onError("tunnel closed");
|
|
413
|
+
}
|
|
414
|
+
if (interactive) {
|
|
415
|
+
for (const [streamId, handlers] of [...this.wss]) {
|
|
416
|
+
this.wss.delete(streamId);
|
|
417
|
+
this.wsRx.drop(streamId);
|
|
418
|
+
handlers.onClose(1006, "tunnel closed");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
335
421
|
}
|
|
336
422
|
allocId() {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
switch (
|
|
344
|
-
case 4
|
|
345
|
-
this.https.get(
|
|
423
|
+
const id = this.nextStreamId;
|
|
424
|
+
this.nextStreamId = this.nextStreamId + 1 >>> 0 || 1;
|
|
425
|
+
return id;
|
|
426
|
+
}
|
|
427
|
+
onFrame(data) {
|
|
428
|
+
const { op, streamId, payload } = decodeFrame(data);
|
|
429
|
+
switch (op) {
|
|
430
|
+
case 4 /* HttpResHead */:
|
|
431
|
+
this.https.get(streamId)?.onHead(payloadJson(payload));
|
|
346
432
|
break;
|
|
347
|
-
case 5
|
|
348
|
-
this.https.get(
|
|
433
|
+
case 5 /* HttpResBody */:
|
|
434
|
+
this.https.get(streamId)?.onBody(payload.slice());
|
|
349
435
|
break;
|
|
350
|
-
case 6
|
|
351
|
-
|
|
436
|
+
case 6 /* HttpResEnd */:
|
|
437
|
+
this.https.get(streamId)?.onEnd();
|
|
438
|
+
this.https.delete(streamId);
|
|
439
|
+
this.httpLane.delete(streamId);
|
|
352
440
|
break;
|
|
353
|
-
case 12
|
|
354
|
-
|
|
355
|
-
if (
|
|
441
|
+
case 12 /* Error */: {
|
|
442
|
+
const waiter = this.https.get(streamId);
|
|
443
|
+
if (waiter) {
|
|
444
|
+
waiter.onError(payloadJson(payload).message);
|
|
445
|
+
this.https.delete(streamId);
|
|
446
|
+
this.httpLane.delete(streamId);
|
|
447
|
+
}
|
|
356
448
|
break;
|
|
357
449
|
}
|
|
358
|
-
case 8
|
|
359
|
-
|
|
360
|
-
this.wss.get(
|
|
450
|
+
case 8 /* WsOpenAck */: {
|
|
451
|
+
const info = payloadJson(payload);
|
|
452
|
+
this.wss.get(streamId)?.onOpenAck(info.ok, info.protocol);
|
|
361
453
|
break;
|
|
362
454
|
}
|
|
363
|
-
case 13
|
|
364
|
-
this.wsRx.cont(
|
|
455
|
+
case 13 /* WsCont */:
|
|
456
|
+
this.wsRx.cont(streamId, payload);
|
|
365
457
|
break;
|
|
366
|
-
case 9
|
|
367
|
-
this.wss.get(
|
|
458
|
+
case 9 /* WsText */:
|
|
459
|
+
this.wss.get(streamId)?.onText(payloadText(this.wsRx.finish(streamId, payload)));
|
|
368
460
|
break;
|
|
369
|
-
case 10
|
|
370
|
-
this.wss.get(
|
|
461
|
+
case 10 /* WsBin */:
|
|
462
|
+
this.wss.get(streamId)?.onBin(this.wsRx.finish(streamId, payload).slice());
|
|
371
463
|
break;
|
|
372
|
-
case 11
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
464
|
+
case 11 /* WsClose */: {
|
|
465
|
+
const info = payloadJson(payload);
|
|
466
|
+
this.wsRx.drop(streamId);
|
|
467
|
+
this.wss.get(streamId)?.onClose(info.code ?? 1000, info.reason ?? "");
|
|
468
|
+
this.wss.delete(streamId);
|
|
377
469
|
break;
|
|
378
470
|
}
|
|
379
471
|
}
|
|
380
472
|
}
|
|
381
|
-
fetch(
|
|
382
|
-
|
|
383
|
-
return new Promise((
|
|
384
|
-
let
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
this.https.set(
|
|
393
|
-
onHead: (
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
if (
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
);
|
|
473
|
+
fetch(method, path, headers, body) {
|
|
474
|
+
const streamId = this.allocId();
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
let head = null;
|
|
477
|
+
let controller = null;
|
|
478
|
+
const stream = new ReadableStream({
|
|
479
|
+
start: (c) => {
|
|
480
|
+
controller = c;
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
const reqHeaders = typeof DecompressionStream !== "undefined" ? { ...headers, "x-codehost-accept-gzip": "1" } : headers;
|
|
484
|
+
this.https.set(streamId, {
|
|
485
|
+
onHead: (h) => {
|
|
486
|
+
head = h;
|
|
487
|
+
const resHeaders = new Headers(h.headers);
|
|
488
|
+
let bodyStream = stream;
|
|
489
|
+
if (resHeaders.get("content-encoding") === "gzip") {
|
|
490
|
+
bodyStream = stream.pipeThrough(new DecompressionStream("gzip"));
|
|
491
|
+
resHeaders.delete("content-encoding");
|
|
492
|
+
resHeaders.delete("content-length");
|
|
493
|
+
}
|
|
494
|
+
resolve(new Response(bodyStream, {
|
|
495
|
+
status: h.status === 204 || h.status === 304 ? h.status : h.status,
|
|
496
|
+
statusText: h.statusText,
|
|
497
|
+
headers: resHeaders
|
|
498
|
+
}));
|
|
408
499
|
},
|
|
409
|
-
onBody: (
|
|
500
|
+
onBody: (b) => {
|
|
410
501
|
try {
|
|
411
|
-
|
|
502
|
+
controller?.enqueue(b);
|
|
412
503
|
} catch {}
|
|
413
504
|
},
|
|
414
505
|
onEnd: () => {
|
|
415
506
|
try {
|
|
416
|
-
|
|
507
|
+
controller?.close();
|
|
417
508
|
} catch {}
|
|
418
|
-
if (!
|
|
509
|
+
if (!head)
|
|
510
|
+
reject(new Error("stream ended before head"));
|
|
419
511
|
},
|
|
420
|
-
onError: (
|
|
512
|
+
onError: (msg) => {
|
|
421
513
|
try {
|
|
422
|
-
|
|
514
|
+
controller?.error(new Error(msg));
|
|
423
515
|
} catch {}
|
|
424
|
-
if (!
|
|
425
|
-
|
|
516
|
+
if (!head)
|
|
517
|
+
reject(new Error(msg));
|
|
518
|
+
}
|
|
426
519
|
});
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
520
|
+
const lane = this.bulk?.isOpen() ? this.bulk : this.transport;
|
|
521
|
+
this.httpLane.set(streamId, lane);
|
|
522
|
+
this.sendOn(lane, encodeJson(1 /* HttpReq */, streamId, { method, path, headers: reqHeaders }));
|
|
523
|
+
if (body && body.byteLength) {
|
|
524
|
+
for (const part of chunk(body))
|
|
525
|
+
this.sendOn(lane, encodeFrame(2 /* HttpReqBody */, streamId, part));
|
|
526
|
+
}
|
|
527
|
+
this.sendOn(lane, encodeFrame(3 /* HttpReqEnd */, streamId));
|
|
431
528
|
});
|
|
432
529
|
}
|
|
433
|
-
openWs(
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
{
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
}
|
|
530
|
+
openWs(path, protocols, handlers) {
|
|
531
|
+
const streamId = this.allocId();
|
|
532
|
+
this.wss.set(streamId, handlers);
|
|
533
|
+
this.send(encodeJson(7 /* WsOpen */, streamId, { path, protocols }));
|
|
534
|
+
return {
|
|
535
|
+
sendText: (text) => {
|
|
536
|
+
for (const f of wsMessageFrames(9 /* WsText */, streamId, this.textEncoder.encode(text)))
|
|
537
|
+
this.send(f);
|
|
538
|
+
},
|
|
539
|
+
sendBin: (data) => {
|
|
540
|
+
for (const f of wsMessageFrames(10 /* WsBin */, streamId, data))
|
|
541
|
+
this.send(f);
|
|
542
|
+
},
|
|
543
|
+
close: (code, reason) => {
|
|
544
|
+
this.send(encodeJson(11 /* WsClose */, streamId, { code, reason }));
|
|
545
|
+
this.wss.delete(streamId);
|
|
448
546
|
}
|
|
449
|
-
|
|
547
|
+
};
|
|
450
548
|
}
|
|
451
|
-
send(
|
|
452
|
-
this.sendOn(this.
|
|
549
|
+
send(frame) {
|
|
550
|
+
this.sendOn(this.transport, frame);
|
|
453
551
|
}
|
|
454
|
-
sendOn(
|
|
455
|
-
if (
|
|
456
|
-
|
|
457
|
-
(Y.set(Q), z.send(Y.buffer));
|
|
458
|
-
}
|
|
552
|
+
sendOn(t, frame) {
|
|
553
|
+
if (t.isOpen())
|
|
554
|
+
t.send(frame);
|
|
459
555
|
}
|
|
460
556
|
get ready() {
|
|
461
|
-
return this.
|
|
557
|
+
return this.transport.isOpen();
|
|
462
558
|
}
|
|
463
559
|
}
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
560
|
+
|
|
561
|
+
// src/tunnel/rtc-datachannel.ts
|
|
562
|
+
function rtcDataChannelTransport(channel) {
|
|
563
|
+
channel.binaryType = "arraybuffer";
|
|
564
|
+
return {
|
|
565
|
+
send(frame) {
|
|
566
|
+
const copy = new Uint8Array(frame.byteLength);
|
|
567
|
+
copy.set(frame);
|
|
568
|
+
channel.send(copy.buffer);
|
|
569
|
+
},
|
|
570
|
+
isOpen: () => channel.readyState === "open",
|
|
571
|
+
bufferedAmount: () => channel.bufferedAmount,
|
|
572
|
+
setBufferedAmountLow(bytes, cb) {
|
|
573
|
+
channel.bufferedAmountLowThreshold = bytes;
|
|
574
|
+
channel.addEventListener("bufferedamountlow", cb);
|
|
575
|
+
},
|
|
576
|
+
onFrame(cb) {
|
|
577
|
+
channel.addEventListener("message", (ev) => {
|
|
578
|
+
if (typeof ev.data === "string")
|
|
579
|
+
return;
|
|
580
|
+
cb(new Uint8Array(ev.data));
|
|
581
|
+
});
|
|
582
|
+
},
|
|
583
|
+
onClose(cb) {
|
|
584
|
+
channel.addEventListener("close", cb);
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
// src/web/tunnel-client.ts
|
|
590
|
+
class TunnelClient2 extends TunnelClient {
|
|
591
|
+
constructor(channel, bulk = null) {
|
|
592
|
+
super(rtcDataChannelTransport(channel), bulk ? rtcDataChannelTransport(bulk) : null);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// src/web/room-client.ts
|
|
597
|
+
var DEFAULT_SIGNAL_URL = "wss://signal.codehost.dev";
|
|
598
|
+
var DIAL_FAIL_COOLDOWN_MS = 1e4;
|
|
599
|
+
|
|
600
|
+
class CodehostRoom {
|
|
467
601
|
peers = [];
|
|
468
602
|
signaling;
|
|
469
|
-
rtcs = new Map
|
|
470
|
-
tunnels = new Map
|
|
471
|
-
dialFailedAt = new Map
|
|
472
|
-
closed =
|
|
473
|
-
constructor(
|
|
474
|
-
|
|
475
|
-
url:
|
|
476
|
-
token:
|
|
477
|
-
role:
|
|
478
|
-
onOpen: () =>
|
|
479
|
-
onClose: () =>
|
|
480
|
-
onPeers: (
|
|
481
|
-
|
|
603
|
+
rtcs = new Map;
|
|
604
|
+
tunnels = new Map;
|
|
605
|
+
dialFailedAt = new Map;
|
|
606
|
+
closed = false;
|
|
607
|
+
constructor(opts) {
|
|
608
|
+
this.signaling = new SignalingClient({
|
|
609
|
+
url: opts.signalUrl ?? DEFAULT_SIGNAL_URL,
|
|
610
|
+
token: opts.token,
|
|
611
|
+
role: CLIENT_WIRE_ROLE,
|
|
612
|
+
onOpen: () => opts.onStatus?.(true),
|
|
613
|
+
onClose: () => opts.onStatus?.(false),
|
|
614
|
+
onPeers: (peers) => {
|
|
615
|
+
this.peers = peers.filter((p) => p.role === "server");
|
|
616
|
+
opts.onPeers?.(this.peers);
|
|
482
617
|
},
|
|
483
|
-
onSignal: (
|
|
484
|
-
})
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
async fetch(
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
return
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
(this.
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
618
|
+
onSignal: (from, data) => void this.rtcs.get(from)?.handleSignal(data)
|
|
619
|
+
});
|
|
620
|
+
this.signaling.connect();
|
|
621
|
+
}
|
|
622
|
+
async fetch(peerId, method, path, init = {}) {
|
|
623
|
+
const tunnel = await this.dial(peerId);
|
|
624
|
+
const body = typeof init.body === "string" ? new TextEncoder().encode(init.body) : init.body;
|
|
625
|
+
return tunnel.fetch(method, path, init.headers ?? {}, body);
|
|
626
|
+
}
|
|
627
|
+
async openWs(peerId, path, protocols, handlers) {
|
|
628
|
+
const tunnel = await this.dial(peerId);
|
|
629
|
+
return tunnel.openWs(path, protocols, handlers);
|
|
630
|
+
}
|
|
631
|
+
dial(peerId) {
|
|
632
|
+
const existing = this.tunnels.get(peerId);
|
|
633
|
+
if (existing)
|
|
634
|
+
return existing;
|
|
635
|
+
const failedAt = this.dialFailedAt.get(peerId);
|
|
636
|
+
if (failedAt != null && Date.now() - failedAt < DIAL_FAIL_COOLDOWN_MS) {
|
|
637
|
+
return Promise.reject(new Error("dial failed recently; cooling down"));
|
|
638
|
+
}
|
|
639
|
+
const drop = () => {
|
|
640
|
+
this.tunnels.delete(peerId);
|
|
641
|
+
this.rtcs.get(peerId)?.close();
|
|
642
|
+
this.rtcs.delete(peerId);
|
|
643
|
+
};
|
|
644
|
+
const dialing = new Promise((resolve, reject) => {
|
|
645
|
+
const timer = setTimeout(() => {
|
|
646
|
+
drop();
|
|
647
|
+
reject(new Error("dial timed out"));
|
|
648
|
+
}, 15000);
|
|
649
|
+
const rtc = new RtcClient({
|
|
650
|
+
sendSignal: (data) => this.signaling.sendSignal(peerId, data),
|
|
651
|
+
onOpen: (channel) => {
|
|
652
|
+
clearTimeout(timer);
|
|
653
|
+
this.dialFailedAt.delete(peerId);
|
|
654
|
+
resolve(new TunnelClient2(channel, rtc.bulkChannel));
|
|
655
|
+
},
|
|
656
|
+
onClose: drop,
|
|
657
|
+
onState: (state) => {
|
|
658
|
+
if (state === "failed" || state === "disconnected")
|
|
659
|
+
drop();
|
|
660
|
+
}
|
|
519
661
|
});
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
(
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
);
|
|
662
|
+
this.rtcs.set(peerId, rtc);
|
|
663
|
+
rtc.start().catch((err) => {
|
|
664
|
+
clearTimeout(timer);
|
|
665
|
+
drop();
|
|
666
|
+
reject(err);
|
|
667
|
+
});
|
|
668
|
+
});
|
|
669
|
+
this.tunnels.set(peerId, dialing);
|
|
670
|
+
dialing.catch(() => {
|
|
671
|
+
this.dialFailedAt.set(peerId, Date.now());
|
|
672
|
+
this.tunnels.delete(peerId);
|
|
673
|
+
});
|
|
674
|
+
return dialing;
|
|
527
675
|
}
|
|
528
676
|
close() {
|
|
529
|
-
if (this.closed)
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
677
|
+
if (this.closed)
|
|
678
|
+
return;
|
|
679
|
+
this.closed = true;
|
|
680
|
+
for (const rtc of this.rtcs.values())
|
|
681
|
+
rtc.close();
|
|
682
|
+
this.rtcs.clear();
|
|
683
|
+
this.tunnels.clear();
|
|
684
|
+
this.signaling.close();
|
|
533
685
|
}
|
|
534
686
|
}
|
|
535
|
-
function
|
|
536
|
-
return new
|
|
687
|
+
function joinRoom(opts) {
|
|
688
|
+
return new CodehostRoom(opts);
|
|
537
689
|
}
|
|
538
|
-
export {
|
|
690
|
+
export {
|
|
691
|
+
joinRoom,
|
|
692
|
+
DEFAULT_SIGNAL_URL,
|
|
693
|
+
CodehostRoom
|
|
694
|
+
};
|