@tapflowio/relay 0.19.0 → 0.20.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/RelayServer.d.ts +37 -1
- package/dist/RelayServer.d.ts.map +1 -1
- package/dist/RelayServer.js +121 -7
- package/dist/RelayServer.js.map +1 -1
- package/dist/SessionManager.d.ts.map +1 -1
- package/dist/SessionManager.js +4 -0
- package/dist/SessionManager.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/cert/index.d.ts +1 -1
- package/dist/lib/cert/index.d.ts.map +1 -1
- package/dist/lib/cert/index.js +1 -1
- package/dist/lib/cert/index.js.map +1 -1
- package/dist/lib/cert/parseCert.d.ts +4 -0
- package/dist/lib/cert/parseCert.d.ts.map +1 -1
- package/dist/lib/cert/parseCert.js +68 -0
- package/dist/lib/cert/parseCert.js.map +1 -1
- package/dist/lib/trailingRequester.d.ts +23 -0
- package/dist/lib/trailingRequester.d.ts.map +1 -0
- package/dist/lib/trailingRequester.js +56 -0
- package/dist/lib/trailingRequester.js.map +1 -0
- package/dist/server.js +4 -2
- package/dist/server.js.map +1 -1
- package/package.json +3 -3
- package/public/assets/{AppCenter-DyP0aqfF.js → AppCenter-Dg4dscW-.js} +5 -5
- package/public/assets/AppCenter-Dg4dscW-.js.br +0 -0
- package/public/assets/{Default-D7ScPE30.js → Default-Dxi8aZ1t.js} +1 -1
- package/public/assets/Default-Dxi8aZ1t.js.br +0 -0
- package/public/assets/{Invite-UmhV5rAf.js → Invite-0EwoClX6.js} +1 -1
- package/public/assets/Invite-0EwoClX6.js.br +0 -0
- package/public/assets/{MacResources-MS4E00w3.js → MacResources-Dy1M3EqK.js} +1 -1
- package/public/assets/MacResources-Dy1M3EqK.js.br +0 -0
- package/public/assets/QASession-DqMj8JL1.js +140 -0
- package/public/assets/QASession-DqMj8JL1.js.br +0 -0
- package/public/assets/{ResetPassword-CAlB6CfP.js → ResetPassword-CXU2bRry.js} +1 -1
- package/public/assets/ResetPassword-CXU2bRry.js.br +0 -0
- package/public/assets/{Setup-CPm2-hPJ.js → Setup-XXChRxeL.js} +1 -1
- package/public/assets/Setup-XXChRxeL.js.br +0 -0
- package/public/assets/{Team-GEIzrM1D.js → Team-D0bXpmEY.js} +2 -2
- package/public/assets/Team-D0bXpmEY.js.br +0 -0
- package/public/assets/{Tokens-DSeBz_CJ.js → Tokens-D0YNHS5e.js} +1 -1
- package/public/assets/Tokens-D0YNHS5e.js.br +0 -0
- package/public/assets/{alert-dialog-BsXI9SVV.js → alert-dialog-CgatMXjB.js} +1 -1
- package/public/assets/alert-dialog-CgatMXjB.js.br +0 -0
- package/public/assets/{badge-BkdYQFsh.js → badge-B6Nf3A0Y.js} +1 -1
- package/public/assets/badge-B6Nf3A0Y.js.br +0 -0
- package/public/assets/{build-format-CXuQt1X2.js → build-format-BFC8gLoh.js} +3 -3
- package/public/assets/build-format-BFC8gLoh.js.br +0 -0
- package/public/assets/{dialog-DuXZpbiF.js → dialog-DK9a3b4z.js} +3 -3
- package/public/assets/dialog-DK9a3b4z.js.br +0 -0
- package/public/assets/{index-bitr6DIC.js → index-9vdYIm7s.js} +66 -71
- package/public/assets/index-9vdYIm7s.js.br +0 -0
- package/public/assets/index-ClHUQm-F.css +1 -0
- package/public/assets/index-ClHUQm-F.css.br +0 -0
- package/public/assets/{pencil-iRR275s8.js → pencil-BeLNLEKe.js} +2 -2
- package/public/assets/pencil-BeLNLEKe.js.br +0 -0
- package/public/assets/{table-CHmhGpHG.js → table-DW57-o1p.js} +1 -1
- package/public/assets/table-DW57-o1p.js.br +0 -0
- package/public/assets/{tabs-BnbeuAOe.js → tabs-Xlttuqxh.js} +1 -1
- package/public/assets/tabs-Xlttuqxh.js.br +0 -0
- package/public/assets/{trash-2-QI162w2K.js → trash-2-CGxISX8x.js} +3 -3
- package/public/assets/trash-2-CGxISX8x.js.br +0 -0
- package/public/index.html +2 -2
- package/public/index.html.br +0 -0
- package/public/assets/AppCenter-DyP0aqfF.js.br +0 -0
- package/public/assets/Default-D7ScPE30.js.br +0 -0
- package/public/assets/Invite-UmhV5rAf.js.br +0 -0
- package/public/assets/MacResources-MS4E00w3.js.br +0 -0
- package/public/assets/QASession-CjgJHDZ3.js +0 -125
- package/public/assets/QASession-CjgJHDZ3.js.br +0 -0
- package/public/assets/ResetPassword-CAlB6CfP.js.br +0 -0
- package/public/assets/Setup-CPm2-hPJ.js.br +0 -0
- package/public/assets/Team-GEIzrM1D.js.br +0 -0
- package/public/assets/Tokens-DSeBz_CJ.js.br +0 -0
- package/public/assets/alert-dialog-BsXI9SVV.js.br +0 -0
- package/public/assets/badge-BkdYQFsh.js.br +0 -0
- package/public/assets/build-format-CXuQt1X2.js.br +0 -0
- package/public/assets/dialog-DuXZpbiF.js.br +0 -0
- package/public/assets/index-Bk2hqaHZ.css +0 -1
- package/public/assets/index-Bk2hqaHZ.css.br +0 -0
- package/public/assets/index-bitr6DIC.js.br +0 -0
- package/public/assets/pencil-iRR275s8.js.br +0 -0
- package/public/assets/table-CHmhGpHG.js.br +0 -0
- package/public/assets/tabs-BnbeuAOe.js.br +0 -0
- package/public/assets/trash-2-QI162w2K.js.br +0 -0
package/dist/RelayServer.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export declare function isExternalAddress(addr: string): boolean;
|
|
|
3
3
|
* a test carrying its own copy of the number passes when the constant moves, which is the drift the
|
|
4
4
|
* wire-contract work keeps finding. Not re-exported from the package index. */
|
|
5
5
|
export declare const IDR_REQUEST_THROTTLE_MS = 500;
|
|
6
|
+
/** The same window for `network:request-state`, and a **separate constant on purpose**: the two share
|
|
7
|
+
* a number today and not a policy. IDR drops inside the window; this one coalesces onto its trailing
|
|
8
|
+
* edge (see `networkStateRequester`). Sharing the constant would mean tuning one tunes the other. */
|
|
9
|
+
export declare const NETWORK_STATE_REQUEST_THROTTLE_MS = 500;
|
|
6
10
|
/**
|
|
7
11
|
* The owner key a connection speaks for, from the authenticated user and the client id it claimed.
|
|
8
12
|
*
|
|
@@ -72,6 +76,9 @@ export declare class RelayServer {
|
|
|
72
76
|
private audioDropHandlers;
|
|
73
77
|
private droppers;
|
|
74
78
|
private idrRequesters;
|
|
79
|
+
/** Unlike `idrRequesters` these hold a timer, which is why the value is not a bare closure — see
|
|
80
|
+
* `forgetSessionState`. */
|
|
81
|
+
private networkStateRequesters;
|
|
75
82
|
private wsRoles;
|
|
76
83
|
/** Per-socket throttle state for `logInboundRejection`. A `WeakMap` so a closed socket's entry goes
|
|
77
84
|
* with the socket — there is no cleanup to forget, unlike the maps keyed by session id nearby. */
|
|
@@ -134,8 +141,17 @@ export declare class RelayServer {
|
|
|
134
141
|
* Pre-existing, and this slice is what makes it worth fixing here rather than filing: `idrRequesters`
|
|
135
142
|
* used to be populated only by the binary drop path, i.e. only under backpressure, and a re-join now
|
|
136
143
|
* creates an entry for any streaming session (#515). The leak went from rare to ordinary.
|
|
144
|
+
*
|
|
145
|
+
* **Now five maps, and the fifth is not dropped the same way.** `networkStateRequesters` holds a
|
|
146
|
+
* *coalescing* requester, so its closure owns a pending `setTimeout` — and deleting a map entry does
|
|
147
|
+
* not cancel a timer. Leaving it armed is not a leak but a double send: `session:leave` followed by a
|
|
148
|
+
* re-join inside the window fires the orphaned trailing edge *and* the new requester's leading edge,
|
|
149
|
+
* which is the second budget for one session that this map exists to prevent. Hence `dispose()`
|
|
150
|
+
* rather than `delete` alone. It was named `forgetSessionStreamState` while all four were stream
|
|
151
|
+
* records; the network requester is not one, so the name lost the word rather than the entry
|
|
152
|
+
* lost the function.
|
|
137
153
|
*/
|
|
138
|
-
private
|
|
154
|
+
private forgetSessionState;
|
|
139
155
|
/**
|
|
140
156
|
* Throttled callback asking the session's agent for an on-demand IDR (fast drop recovery); ignored by
|
|
141
157
|
* agents that don't support it.
|
|
@@ -148,6 +164,26 @@ export declare class RelayServer {
|
|
|
148
164
|
* past the map. Measured: calling it directly from one caller left every test green.
|
|
149
165
|
*/
|
|
150
166
|
private idrRequester;
|
|
167
|
+
/**
|
|
168
|
+
* Throttled callback asking the session's agent to re-read and report the device's network
|
|
169
|
+
* condition (#614); ignored by agents that don't support it.
|
|
170
|
+
*
|
|
171
|
+
* **Coalescing, where `idrRequester` above drops — and the difference is not a preference.** A
|
|
172
|
+
* dropped IDR request costs nothing: the next periodic keyframe repairs it. Nothing re-produces a
|
|
173
|
+
* `network:state`, so a request dropped inside the window leaves that viewer rendering "unknown"
|
|
174
|
+
* for the life of the session. The trailing edge fires after the last join in a burst, and the
|
|
175
|
+
* relay addresses the reply to whichever socket holds the session *when it arrives* — so the
|
|
176
|
+
* viewer that ends up watching is the one served, which a leading edge cannot promise.
|
|
177
|
+
*
|
|
178
|
+
* The burst this is for is the dirty blip, not two people: `join()` refuses a live holder, and a
|
|
179
|
+
* clean `session:leave` runs `forgetSessionState` and builds a fresh requester. A browser socket
|
|
180
|
+
* that dies in its close handler does neither, so `lastAt` survives into the re-join — which is
|
|
181
|
+
* the very path this slice exists to serve.
|
|
182
|
+
*
|
|
183
|
+
* One per session, constructed here, for the reason written on `idrRequester`: the throttle lives
|
|
184
|
+
* in the closure, so a second requester is a second budget.
|
|
185
|
+
*/
|
|
186
|
+
private networkStateRequester;
|
|
151
187
|
private remoteAddressOf;
|
|
152
188
|
private warnProxyMisconfigOnce;
|
|
153
189
|
private handleConnection;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"RelayServer.d.ts","sourceRoot":"","sources":["../src/RelayServer.ts"],"names":[],"mappings":"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"}
|
package/dist/RelayServer.js
CHANGED
|
@@ -14,6 +14,7 @@ import { resolveClientAddress } from './lib/clientAddress.js';
|
|
|
14
14
|
import { resolveCorsHeaders } from './lib/cors.js';
|
|
15
15
|
import { isCsrfBlocked } from './lib/csrf.js';
|
|
16
16
|
import { pickLanAddress } from './lib/lanAddress.js';
|
|
17
|
+
import { createTrailingRequester, systemTimerScheduler } from './lib/trailingRequester.js';
|
|
17
18
|
import { getDb } from './db.js';
|
|
18
19
|
import { handleLogin, handleLogout, handleMe, handleChangePassword, handleInit, handleAuthStatus } from './api/auth.js';
|
|
19
20
|
import { handleVerify, handleAccept } from './api/invitations.js';
|
|
@@ -43,6 +44,10 @@ export function isExternalAddress(addr) {
|
|
|
43
44
|
* a test carrying its own copy of the number passes when the constant moves, which is the drift the
|
|
44
45
|
* wire-contract work keeps finding. Not re-exported from the package index. */
|
|
45
46
|
export const IDR_REQUEST_THROTTLE_MS = 500;
|
|
47
|
+
/** The same window for `network:request-state`, and a **separate constant on purpose**: the two share
|
|
48
|
+
* a number today and not a policy. IDR drops inside the window; this one coalesces onto its trailing
|
|
49
|
+
* edge (see `networkStateRequester`). Sharing the constant would mean tuning one tunes the other. */
|
|
50
|
+
export const NETWORK_STATE_REQUEST_THROTTLE_MS = 500;
|
|
46
51
|
// Ping every socket each interval; a missed pong window (~2× this) terminates the dead socket.
|
|
47
52
|
const HEARTBEAT_MS = 30_000;
|
|
48
53
|
// How long a session outlives its agent's socket, waiting for that agent to come back (#426).
|
|
@@ -198,6 +203,9 @@ export class RelayServer {
|
|
|
198
203
|
droppers = new Map();
|
|
199
204
|
// Per-session throttled "request an IDR from the agent" callbacks (drop recovery).
|
|
200
205
|
idrRequesters = new Map();
|
|
206
|
+
/** Unlike `idrRequesters` these hold a timer, which is why the value is not a bare closure — see
|
|
207
|
+
* `forgetSessionState`. */
|
|
208
|
+
networkStateRequesters = new Map();
|
|
201
209
|
wsRoles = new Map();
|
|
202
210
|
/** Per-socket throttle state for `logInboundRejection`. A `WeakMap` so a closed socket's entry goes
|
|
203
211
|
* with the socket — there is no cleanup to forget, unlike the maps keyed by session id nearby. */
|
|
@@ -409,6 +417,14 @@ export class RelayServer {
|
|
|
409
417
|
for (const timer of this.agentHolds.values())
|
|
410
418
|
clearTimeout(timer);
|
|
411
419
|
this.agentHolds.clear();
|
|
420
|
+
// Same argument one line up. **Not** because the send would land — `sendTo` drops a socket that is
|
|
421
|
+
// not OPEN, which is why `fire` carries no check of its own — but because `unref()` only excuses a
|
|
422
|
+
// timer from keeping the process alive, and the test runner's process outlives the server anyway.
|
|
423
|
+
// An armed edge therefore still runs, holding a closure over the session map of a relay that has
|
|
424
|
+
// already stopped.
|
|
425
|
+
for (const requester of this.networkStateRequesters.values())
|
|
426
|
+
requester.dispose();
|
|
427
|
+
this.networkStateRequesters.clear();
|
|
412
428
|
return new Promise((resolve, reject) => {
|
|
413
429
|
this.wss.clients.forEach((ws) => ws.terminate());
|
|
414
430
|
this.wss.close(() => {
|
|
@@ -467,12 +483,23 @@ export class RelayServer {
|
|
|
467
483
|
* Pre-existing, and this slice is what makes it worth fixing here rather than filing: `idrRequesters`
|
|
468
484
|
* used to be populated only by the binary drop path, i.e. only under backpressure, and a re-join now
|
|
469
485
|
* creates an entry for any streaming session (#515). The leak went from rare to ordinary.
|
|
486
|
+
*
|
|
487
|
+
* **Now five maps, and the fifth is not dropped the same way.** `networkStateRequesters` holds a
|
|
488
|
+
* *coalescing* requester, so its closure owns a pending `setTimeout` — and deleting a map entry does
|
|
489
|
+
* not cancel a timer. Leaving it armed is not a leak but a double send: `session:leave` followed by a
|
|
490
|
+
* re-join inside the window fires the orphaned trailing edge *and* the new requester's leading edge,
|
|
491
|
+
* which is the second budget for one session that this map exists to prevent. Hence `dispose()`
|
|
492
|
+
* rather than `delete` alone. It was named `forgetSessionStreamState` while all four were stream
|
|
493
|
+
* records; the network requester is not one, so the name lost the word rather than the entry
|
|
494
|
+
* lost the function.
|
|
470
495
|
*/
|
|
471
|
-
|
|
496
|
+
forgetSessionState(sessionId) {
|
|
472
497
|
this.dropHandlers.delete(sessionId);
|
|
473
498
|
this.audioDropHandlers.delete(sessionId);
|
|
474
499
|
this.droppers.delete(sessionId);
|
|
475
500
|
this.idrRequesters.delete(sessionId);
|
|
501
|
+
this.networkStateRequesters.get(sessionId)?.dispose();
|
|
502
|
+
this.networkStateRequesters.delete(sessionId);
|
|
476
503
|
}
|
|
477
504
|
/**
|
|
478
505
|
* Throttled callback asking the session's agent for an on-demand IDR (fast drop recovery); ignored by
|
|
@@ -503,6 +530,48 @@ export class RelayServer {
|
|
|
503
530
|
this.idrRequesters.set(sessionId, requester);
|
|
504
531
|
return requester;
|
|
505
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Throttled callback asking the session's agent to re-read and report the device's network
|
|
535
|
+
* condition (#614); ignored by agents that don't support it.
|
|
536
|
+
*
|
|
537
|
+
* **Coalescing, where `idrRequester` above drops — and the difference is not a preference.** A
|
|
538
|
+
* dropped IDR request costs nothing: the next periodic keyframe repairs it. Nothing re-produces a
|
|
539
|
+
* `network:state`, so a request dropped inside the window leaves that viewer rendering "unknown"
|
|
540
|
+
* for the life of the session. The trailing edge fires after the last join in a burst, and the
|
|
541
|
+
* relay addresses the reply to whichever socket holds the session *when it arrives* — so the
|
|
542
|
+
* viewer that ends up watching is the one served, which a leading edge cannot promise.
|
|
543
|
+
*
|
|
544
|
+
* The burst this is for is the dirty blip, not two people: `join()` refuses a live holder, and a
|
|
545
|
+
* clean `session:leave` runs `forgetSessionState` and builds a fresh requester. A browser socket
|
|
546
|
+
* that dies in its close handler does neither, so `lastAt` survives into the re-join — which is
|
|
547
|
+
* the very path this slice exists to serve.
|
|
548
|
+
*
|
|
549
|
+
* One per session, constructed here, for the reason written on `idrRequester`: the throttle lives
|
|
550
|
+
* in the closure, so a second requester is a second budget.
|
|
551
|
+
*/
|
|
552
|
+
networkStateRequester(sessionId) {
|
|
553
|
+
const existing = this.networkStateRequesters.get(sessionId);
|
|
554
|
+
if (existing)
|
|
555
|
+
return existing;
|
|
556
|
+
const requester = createTrailingRequester({
|
|
557
|
+
scheduler: systemTimerScheduler,
|
|
558
|
+
windowMs: NETWORK_STATE_REQUEST_THROTTLE_MS,
|
|
559
|
+
fire: () => {
|
|
560
|
+
const session = this.sessions.get(sessionId);
|
|
561
|
+
// `sendTo` already drops a socket that is not OPEN, so a closed agent needs no check here —
|
|
562
|
+
// an earlier draft duplicated it and no mutation could tell the copy from the original.
|
|
563
|
+
//
|
|
564
|
+
// The lookup is the guard that remains, and it should be unreachable: **every path that removes
|
|
565
|
+
// a session runs `forgetSessionState`, which disposes this timer.** It is kept as the cheap half
|
|
566
|
+
// of that pair rather than as a live case — a trailing edge is the one call here that outlives
|
|
567
|
+
// the statement scheduling it, so the invariant holding is worth not assuming.
|
|
568
|
+
if (session)
|
|
569
|
+
this.sendTo(session.agentSocket, { type: 'network:request-state', sessionId });
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
this.networkStateRequesters.set(sessionId, requester);
|
|
573
|
+
return requester;
|
|
574
|
+
}
|
|
506
575
|
// Extracted so tests can simulate non-loopback origins (all test traffic is loopback).
|
|
507
576
|
remoteAddressOf(request) {
|
|
508
577
|
return request.socket.remoteAddress ?? '';
|
|
@@ -705,7 +774,7 @@ export class RelayServer {
|
|
|
705
774
|
*/
|
|
706
775
|
settleRole(ws, inbound) {
|
|
707
776
|
// Every reason that carries a type, which is all of them but the two that have none to carry.
|
|
708
|
-
// Missing `bad-payload` here returned `false` before the caller could answer, so the
|
|
777
|
+
// Missing `bad-payload` here returned `false` before the caller could answer, so the thirteen
|
|
709
778
|
// answerable requests were classified correctly and then dropped anyway — the regression this
|
|
710
779
|
// whole path exists to prevent, reintroduced one line above it.
|
|
711
780
|
const type = inbound.ok ? inbound.msg.type
|
|
@@ -842,14 +911,14 @@ export class RelayServer {
|
|
|
842
911
|
case 'session:end': {
|
|
843
912
|
if (this.ownsSession(ws, this.sessions.get(msg.sessionId))) {
|
|
844
913
|
this.sessions.remove(msg.sessionId);
|
|
845
|
-
this.
|
|
914
|
+
this.forgetSessionState(msg.sessionId);
|
|
846
915
|
}
|
|
847
916
|
break;
|
|
848
917
|
}
|
|
849
918
|
case 'session:leave': {
|
|
850
919
|
if (this.ownsSession(ws, this.sessions.get(msg.sessionId))) {
|
|
851
920
|
this.sessions.clearBrowser(msg.sessionId);
|
|
852
|
-
this.
|
|
921
|
+
this.forgetSessionState(msg.sessionId);
|
|
853
922
|
}
|
|
854
923
|
break;
|
|
855
924
|
}
|
|
@@ -962,6 +1031,11 @@ export class RelayServer {
|
|
|
962
1031
|
// Bound to the session's own agent socket, unlike the replies above: this payload
|
|
963
1032
|
// lands on the viewer's host OS clipboard, so a second agent (another Mac on the
|
|
964
1033
|
// same relay) must not be able to address someone else's session.
|
|
1034
|
+
// Bound to the session's own agent socket for the same reason clipboard is: this reply says
|
|
1035
|
+
// whether a **specific tester's** device is on the network, and a second agent must not be
|
|
1036
|
+
// able to tell that tester's viewer something about it.
|
|
1037
|
+
case 'network:state':
|
|
1038
|
+
case 'network:error':
|
|
965
1039
|
case 'clipboard:data':
|
|
966
1040
|
case 'clipboard:write-done':
|
|
967
1041
|
case 'clipboard:error': {
|
|
@@ -1183,6 +1257,22 @@ export class RelayServer {
|
|
|
1183
1257
|
}
|
|
1184
1258
|
break;
|
|
1185
1259
|
}
|
|
1260
|
+
// Ownership is not decoration here: taking a device off the network is the most disruptive
|
|
1261
|
+
// thing a non-holder could do to someone else's session short of shutting it down, and unlike
|
|
1262
|
+
// a shutdown it leaves the device *looking* fine. `dispatchTarget` rather than the weaker
|
|
1263
|
+
// `reachableTarget` `device:shutdown` uses — there is no teardown race to accommodate.
|
|
1264
|
+
case 'network:set': {
|
|
1265
|
+
const net = this.dispatchTarget(ws, msg.sessionId);
|
|
1266
|
+
if (net.ok) {
|
|
1267
|
+
net.session.agentSocket.send(JSON.stringify(msg));
|
|
1268
|
+
}
|
|
1269
|
+
else if (ws.readyState === WebSocket.OPEN) {
|
|
1270
|
+
this.sendTo(ws, {
|
|
1271
|
+
type: 'network:error', sessionId: msg.sessionId, requestId: msg.requestId, message: net.message,
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
break;
|
|
1275
|
+
}
|
|
1186
1276
|
}
|
|
1187
1277
|
}
|
|
1188
1278
|
handleAgentResources(ws, msg) {
|
|
@@ -1313,7 +1403,7 @@ export class RelayServer {
|
|
|
1313
1403
|
}
|
|
1314
1404
|
for (const s of agentSessions) {
|
|
1315
1405
|
this.sessions.remove(s.id);
|
|
1316
|
-
this.
|
|
1406
|
+
this.forgetSessionState(s.id);
|
|
1317
1407
|
}
|
|
1318
1408
|
this.sessions.removeResources(ws);
|
|
1319
1409
|
logger.info(cause === 'replaced'
|
|
@@ -1389,7 +1479,7 @@ export class RelayServer {
|
|
|
1389
1479
|
this.sendTo(s.browserSocket, { type: 'session:terminated', sessionId: s.id, reason: 'agent-disconnected' });
|
|
1390
1480
|
}
|
|
1391
1481
|
this.sessions.remove(s.id);
|
|
1392
|
-
this.
|
|
1482
|
+
this.forgetSessionState(s.id);
|
|
1393
1483
|
}
|
|
1394
1484
|
}
|
|
1395
1485
|
// Only devices without a surviving session get a new one. Passing all of them here is what
|
|
@@ -1535,6 +1625,27 @@ export class RelayServer {
|
|
|
1535
1625
|
// backpressure path already throttles this exact message per session; sharing that requester is
|
|
1536
1626
|
// what keeps the two callers from having two policies.
|
|
1537
1627
|
this.idrRequester(session.id)();
|
|
1628
|
+
// And ask what the device's network is doing (#614). `NetworkState` is agent-produced and is not
|
|
1629
|
+
// among the three replayed above, so without this a viewer that reconnects has no way to learn
|
|
1630
|
+
// whether the device is offline and its control renders in a guessed position.
|
|
1631
|
+
//
|
|
1632
|
+
// **Gated on the capability, unlike the IDR request beside it.** That one is safe to send blind
|
|
1633
|
+
// because an agent without it loses nothing — the next periodic keyframe arrives anyway. This one
|
|
1634
|
+
// has no such repair, so an agent that never answers is indistinguishable from one that failed to
|
|
1635
|
+
// read; a viewer arming a deadline would then render "could not read" on every re-join, for an
|
|
1636
|
+
// agent that already said it cannot do this at all. iOS is exactly that agent until #607's last
|
|
1637
|
+
// slice. Fifty lines up, `session:joined` carries these capabilities for the stated reason that a
|
|
1638
|
+
// consumer must not infer support from a timeout — asking anyway would make the relay do it.
|
|
1639
|
+
//
|
|
1640
|
+
// **Inside `readySent` too, and there as an optimisation rather than a correctness condition.**
|
|
1641
|
+
// `readySent` is the *stream's* readiness: `clearStreamSocket` lowers it while `deviceStatus`
|
|
1642
|
+
// stays `'booted'`, so a booted, genuinely offline device with a dead stream socket is not asked
|
|
1643
|
+
// about here — and the agent would have answered, since it still holds that serial. What makes
|
|
1644
|
+
// skipping safe is not the agent's silence but the viewer's: with `readySent` false the
|
|
1645
|
+
// `device:ready` above is not replayed either, so nothing is rendering a live device to be wrong
|
|
1646
|
+
// about. `deviceStatus` is not the fix — the comment on `device:ready` says why (#440).
|
|
1647
|
+
if (session.agentCapabilities?.includes('network-control'))
|
|
1648
|
+
this.networkStateRequester(session.id)();
|
|
1538
1649
|
}
|
|
1539
1650
|
}
|
|
1540
1651
|
/** Shut the device down because nobody is watching it any more. The idle timer's payload, hoisted out
|
|
@@ -1775,8 +1886,11 @@ export class RelayServer {
|
|
|
1775
1886
|
case 'clipboard:write':
|
|
1776
1887
|
this.sendTo(ws, { type: 'clipboard:error', sessionId, requestId, message });
|
|
1777
1888
|
break;
|
|
1889
|
+
case 'network:set':
|
|
1890
|
+
this.sendTo(ws, { type: 'network:error', sessionId, requestId, message });
|
|
1891
|
+
break;
|
|
1778
1892
|
// The four remaining acked inputs. A `default` rather than four labels because the union is
|
|
1779
|
-
// closed and exhaustive: adding a
|
|
1893
|
+
// closed and exhaustive: adding a fourteenth answerable request without a case here would land
|
|
1780
1894
|
// it on `input:error`, which `answerableRequestsAnswered` is what stops.
|
|
1781
1895
|
default:
|
|
1782
1896
|
this.sendTo(ws, { type: 'input:error', sessionId, requestId, message, reason: 'malformed' });
|