instar 1.3.388 → 1.3.389
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/server/PeerStreamProxy.d.ts +104 -0
- package/dist/server/PeerStreamProxy.d.ts.map +1 -0
- package/dist/server/PeerStreamProxy.js +270 -0
- package/dist/server/PeerStreamProxy.js.map +1 -0
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.389.md +45 -0
- package/upgrades/side-effects/dashboard-stream-phase1-proxy.md +61 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerStreamProxy — phase 1 of Pool Dashboard Streaming
|
|
3
|
+
* (POOL-DASHBOARD-STREAM-SPEC §2.2, the scalability core).
|
|
4
|
+
*
|
|
5
|
+
* ONE multiplexed upstream connection per PEER machine, shared by every local
|
|
6
|
+
* dashboard client and every remote session on that peer. The local
|
|
7
|
+
* WebSocketManager (phase 2) owns local clients and the browser WS; this class
|
|
8
|
+
* owns ONLY the single upstream link and the bookkeeping that makes it correct:
|
|
9
|
+
*
|
|
10
|
+
* - reference-counted subscriptions (per (session) → set of local clientIds);
|
|
11
|
+
* - a 60s idle grace after the last unsubscribe, cancelled if a subscribe
|
|
12
|
+
* arrives during it (no thrash opening/closing on a flapping client);
|
|
13
|
+
* - ONE bounded reconnect on an upstream drop, resubscribing every current
|
|
14
|
+
* subscription; a reconnect that does not open within reconnectTimeoutMs, or
|
|
15
|
+
* a second drop, surfaces `machine-unreachable` (P19 — never a reconnect
|
|
16
|
+
* storm);
|
|
17
|
+
* - subscriptions that arrive mid-(re)connect are QUEUED and merged into the
|
|
18
|
+
* resubscribe batch the moment the link opens — never lost;
|
|
19
|
+
* - the peer URL is re-resolved on every subscribe; a changed URL tears the
|
|
20
|
+
* old link down and opens a new one (no stale-URL duplicate links).
|
|
21
|
+
*
|
|
22
|
+
* Everything external is injected (transport, timers, clock) so the whole state
|
|
23
|
+
* machine is deterministically unit-testable without real sockets or wall time.
|
|
24
|
+
* This module performs NO tmux capture and NO local fan-out polling — capture
|
|
25
|
+
* happens only on the owning machine; fan-out to local clients is the injected
|
|
26
|
+
* `onFrameToClients`. (TAP POINT, ops#1: remote subs never enter the local
|
|
27
|
+
* polling loop.)
|
|
28
|
+
*/
|
|
29
|
+
export type ProxyState = 'idle' | 'connecting' | 'active' | 'idle-scheduled' | 'closing' | 'closed';
|
|
30
|
+
/** A live upstream connection handle (one peer's /ws/pool-stream). */
|
|
31
|
+
export interface UpstreamTransport {
|
|
32
|
+
/** Send a frame to the peer (subscribe/unsubscribe/input/key/history). */
|
|
33
|
+
send(frame: Record<string, unknown>): void;
|
|
34
|
+
/** Tear the connection down. Idempotent. */
|
|
35
|
+
close(): void;
|
|
36
|
+
}
|
|
37
|
+
/** Handlers the proxy gives the transport at connect time. */
|
|
38
|
+
export interface UpstreamHandlers {
|
|
39
|
+
/** The link opened and is ready to carry frames. */
|
|
40
|
+
onOpen(): void;
|
|
41
|
+
/** A frame arrived from the peer (output/history/session_ended/error/…). */
|
|
42
|
+
onFrame(frame: Record<string, unknown>): void;
|
|
43
|
+
/** The link closed (clean or error). */
|
|
44
|
+
onClose(): void;
|
|
45
|
+
}
|
|
46
|
+
export type TimerHandle = {
|
|
47
|
+
__brand: 'PeerStreamProxyTimer';
|
|
48
|
+
};
|
|
49
|
+
export interface PeerStreamProxyDeps {
|
|
50
|
+
peerMachineId: string;
|
|
51
|
+
/** Resolve the peer's CURRENT url (re-read each subscribe for URL-change detection). */
|
|
52
|
+
resolveUrl: () => string | null;
|
|
53
|
+
/** Open an upstream link to `url`; the returned transport drives `handlers`. */
|
|
54
|
+
connect: (url: string, handlers: UpstreamHandlers) => UpstreamTransport;
|
|
55
|
+
/** Fan a peer frame out to every LOCAL client subscribed to (session). */
|
|
56
|
+
onFrameToClients: (session: string, frame: Record<string, unknown>) => void;
|
|
57
|
+
/** Surface an error to every local client subscribed to (session). */
|
|
58
|
+
onError: (session: string, code: 'peer-stream-lost' | 'machine-unreachable', detail?: string) => void;
|
|
59
|
+
now: () => number;
|
|
60
|
+
setTimer: (ms: number, fn: () => void) => TimerHandle;
|
|
61
|
+
clearTimer: (t: TimerHandle) => void;
|
|
62
|
+
/** Idle grace before closing an unsubscribed link (default 60_000). */
|
|
63
|
+
idleGraceMs?: number;
|
|
64
|
+
/** Reconnect open deadline (default 10_000). */
|
|
65
|
+
reconnectTimeoutMs?: number;
|
|
66
|
+
logger?: (line: string) => void;
|
|
67
|
+
}
|
|
68
|
+
export declare class PeerStreamProxy {
|
|
69
|
+
private readonly d;
|
|
70
|
+
private state;
|
|
71
|
+
private transport;
|
|
72
|
+
private connectedUrl;
|
|
73
|
+
/** session → set of local clientIds subscribed to it. THE source of truth:
|
|
74
|
+
* on every (re)open we resubscribe from this map, so a subscribe that
|
|
75
|
+
* arrives mid-connect is never lost and a mid-connect unsubscribe is never
|
|
76
|
+
* replayed. */
|
|
77
|
+
private readonly subs;
|
|
78
|
+
private idleTimer;
|
|
79
|
+
private reconnectTimer;
|
|
80
|
+
private reconnectUsed;
|
|
81
|
+
constructor(d: PeerStreamProxyDeps);
|
|
82
|
+
get currentState(): ProxyState;
|
|
83
|
+
/** Total live subscriptions across all sessions (refcount). */
|
|
84
|
+
get refCount(): number;
|
|
85
|
+
/** Snapshot of session → clientIds (tests / introspection). */
|
|
86
|
+
sessionsView(): Record<string, string[]>;
|
|
87
|
+
private log;
|
|
88
|
+
/** A local client subscribes to a remote session on this peer. */
|
|
89
|
+
subscribe(session: string, clientId: string): void;
|
|
90
|
+
/** A local client unsubscribes (or disconnects). */
|
|
91
|
+
unsubscribe(session: string, clientId: string): void;
|
|
92
|
+
/** Forward an input/key frame for a session (caller already gated allowRemoteInput). */
|
|
93
|
+
relayInput(frame: Record<string, unknown>): void;
|
|
94
|
+
private openFresh;
|
|
95
|
+
private onUpstreamOpen;
|
|
96
|
+
private onUpstreamFrame;
|
|
97
|
+
private onUpstreamClose;
|
|
98
|
+
private scheduleIdleClose;
|
|
99
|
+
private failAll;
|
|
100
|
+
private teardownTransport;
|
|
101
|
+
/** Force-close (peer removed, server shutdown). Idempotent. */
|
|
102
|
+
close(): void;
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=PeerStreamProxy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PeerStreamProxy.d.ts","sourceRoot":"","sources":["../../src/server/PeerStreamProxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PeerStreamProxy — phase 1 of Pool Dashboard Streaming
|
|
3
|
+
* (POOL-DASHBOARD-STREAM-SPEC §2.2, the scalability core).
|
|
4
|
+
*
|
|
5
|
+
* ONE multiplexed upstream connection per PEER machine, shared by every local
|
|
6
|
+
* dashboard client and every remote session on that peer. The local
|
|
7
|
+
* WebSocketManager (phase 2) owns local clients and the browser WS; this class
|
|
8
|
+
* owns ONLY the single upstream link and the bookkeeping that makes it correct:
|
|
9
|
+
*
|
|
10
|
+
* - reference-counted subscriptions (per (session) → set of local clientIds);
|
|
11
|
+
* - a 60s idle grace after the last unsubscribe, cancelled if a subscribe
|
|
12
|
+
* arrives during it (no thrash opening/closing on a flapping client);
|
|
13
|
+
* - ONE bounded reconnect on an upstream drop, resubscribing every current
|
|
14
|
+
* subscription; a reconnect that does not open within reconnectTimeoutMs, or
|
|
15
|
+
* a second drop, surfaces `machine-unreachable` (P19 — never a reconnect
|
|
16
|
+
* storm);
|
|
17
|
+
* - subscriptions that arrive mid-(re)connect are QUEUED and merged into the
|
|
18
|
+
* resubscribe batch the moment the link opens — never lost;
|
|
19
|
+
* - the peer URL is re-resolved on every subscribe; a changed URL tears the
|
|
20
|
+
* old link down and opens a new one (no stale-URL duplicate links).
|
|
21
|
+
*
|
|
22
|
+
* Everything external is injected (transport, timers, clock) so the whole state
|
|
23
|
+
* machine is deterministically unit-testable without real sockets or wall time.
|
|
24
|
+
* This module performs NO tmux capture and NO local fan-out polling — capture
|
|
25
|
+
* happens only on the owning machine; fan-out to local clients is the injected
|
|
26
|
+
* `onFrameToClients`. (TAP POINT, ops#1: remote subs never enter the local
|
|
27
|
+
* polling loop.)
|
|
28
|
+
*/
|
|
29
|
+
const DEFAULT_IDLE_GRACE_MS = 60_000;
|
|
30
|
+
const DEFAULT_RECONNECT_TIMEOUT_MS = 10_000;
|
|
31
|
+
export class PeerStreamProxy {
|
|
32
|
+
d;
|
|
33
|
+
state = 'idle';
|
|
34
|
+
transport = null;
|
|
35
|
+
connectedUrl = null;
|
|
36
|
+
/** session → set of local clientIds subscribed to it. THE source of truth:
|
|
37
|
+
* on every (re)open we resubscribe from this map, so a subscribe that
|
|
38
|
+
* arrives mid-connect is never lost and a mid-connect unsubscribe is never
|
|
39
|
+
* replayed. */
|
|
40
|
+
subs = new Map();
|
|
41
|
+
idleTimer = null;
|
|
42
|
+
reconnectTimer = null;
|
|
43
|
+
reconnectUsed = false;
|
|
44
|
+
constructor(d) {
|
|
45
|
+
this.d = d;
|
|
46
|
+
}
|
|
47
|
+
get currentState() {
|
|
48
|
+
return this.state;
|
|
49
|
+
}
|
|
50
|
+
/** Total live subscriptions across all sessions (refcount). */
|
|
51
|
+
get refCount() {
|
|
52
|
+
let n = 0;
|
|
53
|
+
for (const set of this.subs.values())
|
|
54
|
+
n += set.size;
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
/** Snapshot of session → clientIds (tests / introspection). */
|
|
58
|
+
sessionsView() {
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const [s, set] of this.subs)
|
|
61
|
+
out[s] = [...set];
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
log(line) {
|
|
65
|
+
this.d.logger?.(`[peer-stream-proxy ${this.d.peerMachineId}] ${line}`);
|
|
66
|
+
}
|
|
67
|
+
// ── subscribe / unsubscribe (refcounted) ───────────────────────────────
|
|
68
|
+
/** A local client subscribes to a remote session on this peer. */
|
|
69
|
+
subscribe(session, clientId) {
|
|
70
|
+
if (this.state === 'closed') {
|
|
71
|
+
this.log('subscribe after close — ignored');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Cancel a pending idle close — we have demand again.
|
|
75
|
+
if (this.idleTimer) {
|
|
76
|
+
this.d.clearTimer(this.idleTimer);
|
|
77
|
+
this.idleTimer = null;
|
|
78
|
+
if (this.state === 'idle-scheduled')
|
|
79
|
+
this.state = 'active';
|
|
80
|
+
}
|
|
81
|
+
let set = this.subs.get(session);
|
|
82
|
+
if (!set) {
|
|
83
|
+
set = new Set();
|
|
84
|
+
this.subs.set(session, set);
|
|
85
|
+
}
|
|
86
|
+
const firstForSession = set.size === 0;
|
|
87
|
+
set.add(clientId);
|
|
88
|
+
// URL re-resolution (stale-URL guard): if the peer moved, reconnect fresh.
|
|
89
|
+
const url = this.d.resolveUrl();
|
|
90
|
+
if (this.state === 'active' && this.connectedUrl && url && url !== this.connectedUrl) {
|
|
91
|
+
this.log(`peer url changed (${this.connectedUrl} → ${url}); reconnecting`);
|
|
92
|
+
this.teardownTransport();
|
|
93
|
+
this.openFresh(url);
|
|
94
|
+
return; // openFresh replays ALL current subs (incl. this one) on open
|
|
95
|
+
}
|
|
96
|
+
if (this.state === 'idle' || this.state === 'closing') {
|
|
97
|
+
// No link yet → open one; openFresh flushes all subs on open.
|
|
98
|
+
if (!url) {
|
|
99
|
+
// No reachable URL — honest failure, drop the just-added sub for it.
|
|
100
|
+
set.delete(clientId);
|
|
101
|
+
if (set.size === 0)
|
|
102
|
+
this.subs.delete(session);
|
|
103
|
+
this.d.onError(session, 'machine-unreachable', 'no url for peer');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
this.openFresh(url);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (this.state === 'active') {
|
|
110
|
+
// Link is live → forward only when this is the first client for the
|
|
111
|
+
// session (the upstream is already streaming it for any later client).
|
|
112
|
+
if (firstForSession)
|
|
113
|
+
this.transport?.send({ type: 'subscribe', session });
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
// connecting → the sub is now in `subs`; onUpstreamOpen resubscribes from
|
|
117
|
+
// `subs`, so it is automatically included in the batch (never lost).
|
|
118
|
+
}
|
|
119
|
+
/** A local client unsubscribes (or disconnects). */
|
|
120
|
+
unsubscribe(session, clientId) {
|
|
121
|
+
const set = this.subs.get(session);
|
|
122
|
+
if (!set)
|
|
123
|
+
return;
|
|
124
|
+
set.delete(clientId);
|
|
125
|
+
if (set.size === 0) {
|
|
126
|
+
this.subs.delete(session);
|
|
127
|
+
if (this.state === 'active')
|
|
128
|
+
this.transport?.send({ type: 'unsubscribe', session });
|
|
129
|
+
}
|
|
130
|
+
if (this.refCount === 0)
|
|
131
|
+
this.scheduleIdleClose();
|
|
132
|
+
}
|
|
133
|
+
/** Forward an input/key frame for a session (caller already gated allowRemoteInput). */
|
|
134
|
+
relayInput(frame) {
|
|
135
|
+
if (this.state === 'active')
|
|
136
|
+
this.transport?.send(frame);
|
|
137
|
+
// Not active → input is dropped (caller's terminal shows the live state);
|
|
138
|
+
// we never queue keystrokes across a reconnect (stale input is worse than none).
|
|
139
|
+
}
|
|
140
|
+
// ── connection lifecycle ───────────────────────────────────────────────
|
|
141
|
+
openFresh(url, reconnect = false) {
|
|
142
|
+
// A user-initiated open resets the one-shot reconnect budget; a reconnect
|
|
143
|
+
// leaves `reconnectUsed` true so a further drop → machine-unreachable.
|
|
144
|
+
if (!reconnect)
|
|
145
|
+
this.reconnectUsed = false;
|
|
146
|
+
this.state = 'connecting';
|
|
147
|
+
this.connectedUrl = url;
|
|
148
|
+
const handlers = {
|
|
149
|
+
onOpen: () => this.onUpstreamOpen(),
|
|
150
|
+
onFrame: (f) => this.onUpstreamFrame(f),
|
|
151
|
+
onClose: () => this.onUpstreamClose(),
|
|
152
|
+
};
|
|
153
|
+
try {
|
|
154
|
+
this.transport = this.d.connect(url, handlers);
|
|
155
|
+
}
|
|
156
|
+
catch (e) {
|
|
157
|
+
this.log(`connect threw: ${e?.message ?? e}`);
|
|
158
|
+
this.onUpstreamClose();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
onUpstreamOpen() {
|
|
162
|
+
if (this.state === 'closed') {
|
|
163
|
+
this.transport?.close();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.state = 'active';
|
|
167
|
+
if (this.reconnectTimer) {
|
|
168
|
+
this.d.clearTimer(this.reconnectTimer);
|
|
169
|
+
this.reconnectTimer = null;
|
|
170
|
+
}
|
|
171
|
+
// Resubscribe from `subs` (the source of truth) — this naturally includes
|
|
172
|
+
// any subscribe that arrived mid-connect and excludes any mid-connect
|
|
173
|
+
// unsubscribe. No stale snapshot to lose.
|
|
174
|
+
for (const session of this.subs.keys())
|
|
175
|
+
this.transport?.send({ type: 'subscribe', session });
|
|
176
|
+
// If nothing is subscribed anymore (all unsubscribed during connect), idle-close.
|
|
177
|
+
if (this.refCount === 0)
|
|
178
|
+
this.scheduleIdleClose();
|
|
179
|
+
}
|
|
180
|
+
onUpstreamFrame(frame) {
|
|
181
|
+
const session = typeof frame.session === 'string' ? frame.session : null;
|
|
182
|
+
if (!session)
|
|
183
|
+
return; // peer frames without a session are not fan-out-able
|
|
184
|
+
// Only fan out to sessions we still have local subscribers for.
|
|
185
|
+
if (!this.subs.has(session))
|
|
186
|
+
return;
|
|
187
|
+
this.d.onFrameToClients(session, frame);
|
|
188
|
+
}
|
|
189
|
+
onUpstreamClose() {
|
|
190
|
+
if (this.state === 'closed' || this.state === 'closing') {
|
|
191
|
+
this.state = 'closed';
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
this.transport = null;
|
|
195
|
+
// Nothing subscribed → just go idle, no reconnect.
|
|
196
|
+
if (this.refCount === 0) {
|
|
197
|
+
this.state = 'idle';
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (this.reconnectUsed) {
|
|
201
|
+
// Second failure → declare the peer unreachable for every affected session.
|
|
202
|
+
this.log('second upstream drop → machine-unreachable');
|
|
203
|
+
this.failAll('machine-unreachable', 'peer unreachable after reconnect');
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
// First drop → tell clients, attempt ONE bounded reconnect.
|
|
207
|
+
for (const session of this.subs.keys())
|
|
208
|
+
this.d.onError(session, 'peer-stream-lost', 'upstream dropped');
|
|
209
|
+
this.reconnectUsed = true;
|
|
210
|
+
const url = this.d.resolveUrl();
|
|
211
|
+
if (!url) {
|
|
212
|
+
this.failAll('machine-unreachable', 'no url on reconnect');
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
this.openFresh(url, true);
|
|
216
|
+
// Bound the reconnect: if it doesn't open in time, declare unreachable.
|
|
217
|
+
this.reconnectTimer = this.d.setTimer(this.d.reconnectTimeoutMs ?? DEFAULT_RECONNECT_TIMEOUT_MS, () => {
|
|
218
|
+
this.reconnectTimer = null;
|
|
219
|
+
if (this.state !== 'active') {
|
|
220
|
+
this.log('reconnect timed out → machine-unreachable');
|
|
221
|
+
this.teardownTransport();
|
|
222
|
+
this.failAll('machine-unreachable', 'reconnect timed out');
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
scheduleIdleClose() {
|
|
227
|
+
if (this.idleTimer || this.state === 'closed')
|
|
228
|
+
return;
|
|
229
|
+
if (this.state === 'active')
|
|
230
|
+
this.state = 'idle-scheduled';
|
|
231
|
+
this.idleTimer = this.d.setTimer(this.d.idleGraceMs ?? DEFAULT_IDLE_GRACE_MS, () => {
|
|
232
|
+
this.idleTimer = null;
|
|
233
|
+
if (this.refCount === 0) {
|
|
234
|
+
this.log('idle grace elapsed → closing');
|
|
235
|
+
this.teardownTransport();
|
|
236
|
+
this.state = 'closed';
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
failAll(code, detail) {
|
|
241
|
+
for (const session of this.subs.keys())
|
|
242
|
+
this.d.onError(session, code, detail);
|
|
243
|
+
this.subs.clear();
|
|
244
|
+
this.teardownTransport();
|
|
245
|
+
this.state = 'closed';
|
|
246
|
+
}
|
|
247
|
+
teardownTransport() {
|
|
248
|
+
if (this.reconnectTimer) {
|
|
249
|
+
this.d.clearTimer(this.reconnectTimer);
|
|
250
|
+
this.reconnectTimer = null;
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
this.transport?.close();
|
|
254
|
+
}
|
|
255
|
+
catch { /* @silent-fallback-ok: closing an already-dead upstream is best-effort */ }
|
|
256
|
+
this.transport = null;
|
|
257
|
+
this.connectedUrl = null;
|
|
258
|
+
}
|
|
259
|
+
/** Force-close (peer removed, server shutdown). Idempotent. */
|
|
260
|
+
close() {
|
|
261
|
+
if (this.idleTimer) {
|
|
262
|
+
this.d.clearTimer(this.idleTimer);
|
|
263
|
+
this.idleTimer = null;
|
|
264
|
+
}
|
|
265
|
+
this.subs.clear();
|
|
266
|
+
this.teardownTransport();
|
|
267
|
+
this.state = 'closed';
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
//# sourceMappingURL=PeerStreamProxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PeerStreamProxy.js","sourceRoot":"","sources":["../../src/server/PeerStreamProxy.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-06-07T00:
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-06-07T00:51:44.855Z",
|
|
5
|
+
"instarVersion": "1.3.389",
|
|
6
6
|
"entryCount": 199,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
First building block of the single-dashboard cross-machine streaming feature
|
|
9
|
+
(POOL-DASHBOARD-STREAM-SPEC): the `PeerStreamProxy` — one multiplexed upstream
|
|
10
|
+
connection per peer machine that the dashboard's WebSocket layer will use
|
|
11
|
+
(phase 2) to relay a remote session's terminal stream. This phase ships the
|
|
12
|
+
state machine ONLY; it is not yet wired to the live WebSocketManager, so it
|
|
13
|
+
changes no user-visible behavior on its own.
|
|
14
|
+
|
|
15
|
+
The proxy is the scalability + correctness core the reviewers stressed:
|
|
16
|
+
reference-counted subscriptions, a 60s idle grace (cancelled if demand
|
|
17
|
+
returns), ONE bounded reconnect on a drop with full resubscribe (a second drop
|
|
18
|
+
or a reconnect timeout → machine-unreachable, never a reconnect storm),
|
|
19
|
+
mid-reconnect subscribes merged from the source-of-truth subscription map (never
|
|
20
|
+
lost), and peer-URL-change detection (stale-URL links are torn down). Capture
|
|
21
|
+
stays on the owning machine; this only relays — so fleet cost is
|
|
22
|
+
O(subscriptions), not O(sessions × clients × peers).
|
|
23
|
+
|
|
24
|
+
## What to Tell Your User
|
|
25
|
+
|
|
26
|
+
Nothing yet — this is internal plumbing. The visible result (click any
|
|
27
|
+
machine's session and it streams) lands when phases 2–3 wire it to the
|
|
28
|
+
dashboard.
|
|
29
|
+
|
|
30
|
+
- audience: agent-only
|
|
31
|
+
- maturity: preview
|
|
32
|
+
|
|
33
|
+
## Summary of New Capabilities
|
|
34
|
+
|
|
35
|
+
- `src/server/PeerStreamProxy.ts` — the per-peer upstream relay state machine,
|
|
36
|
+
fully transport/timer/clock-injected (no live wiring yet).
|
|
37
|
+
|
|
38
|
+
## Evidence
|
|
39
|
+
|
|
40
|
+
- `tests/unit/PeerStreamProxy.test.ts` (13 tests, deterministic): open+multiplex
|
|
41
|
+
(one upstream, no double-subscribe for a second client), idle-close +
|
|
42
|
+
mid-grace reactivate, bounded reconnect + resubscribe, second-drop →
|
|
43
|
+
unreachable, reconnect-timeout → unreachable, mid-reconnect subscribe merged,
|
|
44
|
+
url-change reopen, no-url → machine-unreachable, input-relay-only-when-active,
|
|
45
|
+
idempotent close.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Side-Effects Review — Pool dashboard streaming phase 1 (PeerStreamProxy)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dashboard-stream-phase1-proxy`
|
|
4
|
+
**Date:** `2026-06-06`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
A NEW, self-contained module (`PeerStreamProxy`) — phase 1 of the converged
|
|
11
|
+
POOL-DASHBOARD-STREAM-SPEC. The per-peer upstream relay state machine, with all
|
|
12
|
+
external surfaces (transport, timers, clock) injected. NOT wired into the live
|
|
13
|
+
WebSocketManager or server boot in this phase, so it has zero runtime effect
|
|
14
|
+
until phase 2 consumes it.
|
|
15
|
+
|
|
16
|
+
## Decision-point inventory
|
|
17
|
+
|
|
18
|
+
The state machine's transitions: open / multiplex / idle-close / reactivate /
|
|
19
|
+
bounded-reconnect / second-drop-unreachable / reconnect-timeout / url-change /
|
|
20
|
+
no-url. Each is covered both-sides by a deterministic test.
|
|
21
|
+
|
|
22
|
+
## 1. Over-block
|
|
23
|
+
|
|
24
|
+
N/A — no gating; a relay. The conservative reconnect policy (one attempt per
|
|
25
|
+
episode, then machine-unreachable) could declare a flaky-but-recoverable peer
|
|
26
|
+
unreachable, but the user simply re-subscribes (re-click), which opens fresh
|
|
27
|
+
and resets the budget. Storm-proof beats persistent.
|
|
28
|
+
|
|
29
|
+
## 2. Under-block
|
|
30
|
+
|
|
31
|
+
Keystrokes are never queued across a (re)connect — stale input is worse than
|
|
32
|
+
none. Acceptable: the dashboard shows the live link state (phase 3) so the user
|
|
33
|
+
knows when input is live.
|
|
34
|
+
|
|
35
|
+
## 3. Level-of-abstraction fit
|
|
36
|
+
|
|
37
|
+
Pure module under src/server/, no I/O of its own. The WebSocketManager (phase 2)
|
|
38
|
+
owns local clients + the browser WS and injects the transport; this owns only
|
|
39
|
+
the single upstream link + its bookkeeping. Matches the P2 precedent of
|
|
40
|
+
shipping a tested building block before its consumer.
|
|
41
|
+
|
|
42
|
+
## 4. Signal vs authority compliance
|
|
43
|
+
|
|
44
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
45
|
+
|
|
46
|
+
No authority. A transport relay; all auth (ticket) + input gating
|
|
47
|
+
(allowRemoteInput) live in phase 2 at the WSManager/upgrade boundary per spec
|
|
48
|
+
§2.3.
|
|
49
|
+
|
|
50
|
+
## 5. Interactions
|
|
51
|
+
|
|
52
|
+
- WebSocketManager (phase 2): the future consumer; unaffected now (no import).
|
|
53
|
+
- Local polling loop: untouched — remote subs never enter it (TAP POINT). This
|
|
54
|
+
module does no tmux capture.
|
|
55
|
+
- P19 (No Unbounded Loops): the reconnect is a single bounded attempt; tests
|
|
56
|
+
assert the second-drop and timeout both terminate to machine-unreachable.
|
|
57
|
+
|
|
58
|
+
## 6. External surfaces
|
|
59
|
+
|
|
60
|
+
None in this phase. No routes, config, notifications, or wiring. A new source
|
|
61
|
+
file + its test only.
|