@gtc6244/immediacy-daemon 0.2.1 → 0.2.3
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/src/crypto.d.ts +18 -39
- package/dist/src/crypto.js +33 -152
- package/dist/src/crypto.js.map +1 -1
- package/dist/src/devices.d.ts +46 -0
- package/dist/src/devices.js +114 -0
- package/dist/src/devices.js.map +1 -0
- package/dist/src/envelope.d.ts +3 -3
- package/dist/src/envelope.js +3 -3
- package/dist/src/forward.d.ts +4 -4
- package/dist/src/forward.js +1 -1
- package/dist/src/forward.js.map +1 -1
- package/dist/src/framing.d.ts +76 -0
- package/dist/src/framing.js +133 -0
- package/dist/src/framing.js.map +1 -0
- package/dist/src/hub.d.ts +29 -9
- package/dist/src/hub.js +20 -5
- package/dist/src/hub.js.map +1 -1
- package/dist/src/index.d.ts +3 -3
- package/dist/src/index.js +79 -17
- package/dist/src/index.js.map +1 -1
- package/dist/src/lan.d.ts +103 -0
- package/dist/src/lan.js +182 -0
- package/dist/src/lan.js.map +1 -0
- package/dist/src/pairing.d.ts +69 -0
- package/dist/src/pairing.js +125 -0
- package/dist/src/pairing.js.map +1 -0
- package/dist/src/protocol.d.ts +66 -23
- package/dist/src/protocol.js +4 -1
- package/dist/src/protocol.js.map +1 -1
- package/dist/src/push.d.ts +104 -0
- package/dist/src/push.js +339 -0
- package/dist/src/push.js.map +1 -0
- package/dist/src/relay-transport.d.ts +62 -0
- package/dist/src/relay-transport.js +180 -0
- package/dist/src/relay-transport.js.map +1 -0
- package/dist/src/relay-wire.d.ts +25 -0
- package/dist/src/relay-wire.js +37 -0
- package/dist/src/relay-wire.js.map +1 -0
- package/dist/src/rendezvous.d.ts +57 -0
- package/dist/src/rendezvous.js +152 -0
- package/dist/src/rendezvous.js.map +1 -0
- package/dist/src/server.d.ts +19 -7
- package/dist/src/server.js +180 -63
- package/dist/src/server.js.map +1 -1
- package/dist/src/session-socket.d.ts +34 -0
- package/dist/src/session-socket.js +84 -0
- package/dist/src/session-socket.js.map +1 -0
- package/dist/src/session.d.ts +151 -0
- package/dist/src/session.js +241 -0
- package/dist/src/session.js.map +1 -0
- package/dist/src/tcp-transport.d.ts +37 -0
- package/dist/src/tcp-transport.js +106 -0
- package/dist/src/tcp-transport.js.map +1 -0
- package/dist/src/telemetry.d.ts +69 -0
- package/dist/src/telemetry.js +64 -0
- package/dist/src/telemetry.js.map +1 -0
- package/dist/src/transport.d.ts +75 -0
- package/dist/src/transport.js +136 -0
- package/dist/src/transport.js.map +1 -0
- package/dist/src/workspace.d.ts +20 -5
- package/dist/src/workspace.js +19 -1
- package/dist/src/workspace.js.map +1 -1
- package/package.json +17 -4
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport telemetry (docs/WEBRTC_MIGRATION.md §5). Starts in Phase 4 because
|
|
3
|
+
* the numbers here drive two decisions: the DO free-tier budget, and whether
|
|
4
|
+
* Phase 6 (WebRTC) is worth building at all.
|
|
5
|
+
*
|
|
6
|
+
* A tiny in-memory recorder with a typed API + optional event sink. It records:
|
|
7
|
+
* - which transport each session used, and failover events;
|
|
8
|
+
* - time-to-connect per transport;
|
|
9
|
+
* - relay message count + byte volume (the DO billing driver);
|
|
10
|
+
* - handshake failures by cause;
|
|
11
|
+
* - reconnects, split by cause (mobile-backgrounding vs network change).
|
|
12
|
+
* No payloads, no identities — counts and durations only.
|
|
13
|
+
*/
|
|
14
|
+
export type TransportKind = "loopback" | "lan" | "relay" | "webrtc";
|
|
15
|
+
export type HandshakeFailureCause = "unknown_device" | "bad_signature" | "expired_code" | "bad_message" | "timeout";
|
|
16
|
+
export type ReconnectCause = "mobile_background" | "network_change" | "daemon_restart" | "other";
|
|
17
|
+
interface ConnectStat {
|
|
18
|
+
count: number;
|
|
19
|
+
totalMs: number;
|
|
20
|
+
maxMs: number;
|
|
21
|
+
}
|
|
22
|
+
export interface TelemetrySnapshot {
|
|
23
|
+
sessionsByTransport: Record<string, number>;
|
|
24
|
+
failovers: number;
|
|
25
|
+
timeToConnectMs: Record<string, ConnectStat>;
|
|
26
|
+
relay: {
|
|
27
|
+
messages: number;
|
|
28
|
+
bytes: number;
|
|
29
|
+
};
|
|
30
|
+
handshakeFailures: Record<string, number>;
|
|
31
|
+
reconnects: Record<string, number>;
|
|
32
|
+
}
|
|
33
|
+
export type TelemetryEvent = {
|
|
34
|
+
kind: "session";
|
|
35
|
+
transport: TransportKind;
|
|
36
|
+
connectMs: number;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "failover";
|
|
39
|
+
from: TransportKind;
|
|
40
|
+
to: TransportKind;
|
|
41
|
+
} | {
|
|
42
|
+
kind: "relay_message";
|
|
43
|
+
bytes: number;
|
|
44
|
+
} | {
|
|
45
|
+
kind: "handshake_failure";
|
|
46
|
+
cause: HandshakeFailureCause;
|
|
47
|
+
} | {
|
|
48
|
+
kind: "reconnect";
|
|
49
|
+
cause: ReconnectCause;
|
|
50
|
+
};
|
|
51
|
+
export declare class Telemetry {
|
|
52
|
+
private sessionsByTransport;
|
|
53
|
+
private failovers;
|
|
54
|
+
private timeToConnect;
|
|
55
|
+
private relayMessages;
|
|
56
|
+
private relayBytes;
|
|
57
|
+
private handshakeFailures;
|
|
58
|
+
private reconnects;
|
|
59
|
+
/** Optional sink for live streaming (e.g. to a metrics endpoint). */
|
|
60
|
+
onEvent?: (event: TelemetryEvent) => void;
|
|
61
|
+
private emit;
|
|
62
|
+
sessionEstablished(transport: TransportKind, connectMs: number): void;
|
|
63
|
+
failover(from: TransportKind, to: TransportKind): void;
|
|
64
|
+
relayMessage(bytes: number): void;
|
|
65
|
+
handshakeFailure(cause: HandshakeFailureCause): void;
|
|
66
|
+
reconnect(cause: ReconnectCause): void;
|
|
67
|
+
snapshot(): TelemetrySnapshot;
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport telemetry (docs/WEBRTC_MIGRATION.md §5). Starts in Phase 4 because
|
|
3
|
+
* the numbers here drive two decisions: the DO free-tier budget, and whether
|
|
4
|
+
* Phase 6 (WebRTC) is worth building at all.
|
|
5
|
+
*
|
|
6
|
+
* A tiny in-memory recorder with a typed API + optional event sink. It records:
|
|
7
|
+
* - which transport each session used, and failover events;
|
|
8
|
+
* - time-to-connect per transport;
|
|
9
|
+
* - relay message count + byte volume (the DO billing driver);
|
|
10
|
+
* - handshake failures by cause;
|
|
11
|
+
* - reconnects, split by cause (mobile-backgrounding vs network change).
|
|
12
|
+
* No payloads, no identities — counts and durations only.
|
|
13
|
+
*/
|
|
14
|
+
export class Telemetry {
|
|
15
|
+
sessionsByTransport = {};
|
|
16
|
+
failovers = 0;
|
|
17
|
+
timeToConnect = {};
|
|
18
|
+
relayMessages = 0;
|
|
19
|
+
relayBytes = 0;
|
|
20
|
+
handshakeFailures = {};
|
|
21
|
+
reconnects = {};
|
|
22
|
+
/** Optional sink for live streaming (e.g. to a metrics endpoint). */
|
|
23
|
+
onEvent;
|
|
24
|
+
emit(event) {
|
|
25
|
+
this.onEvent?.(event);
|
|
26
|
+
}
|
|
27
|
+
sessionEstablished(transport, connectMs) {
|
|
28
|
+
this.sessionsByTransport[transport] = (this.sessionsByTransport[transport] ?? 0) + 1;
|
|
29
|
+
const s = this.timeToConnect[transport] ?? { count: 0, totalMs: 0, maxMs: 0 };
|
|
30
|
+
s.count++;
|
|
31
|
+
s.totalMs += connectMs;
|
|
32
|
+
s.maxMs = Math.max(s.maxMs, connectMs);
|
|
33
|
+
this.timeToConnect[transport] = s;
|
|
34
|
+
this.emit({ kind: "session", transport, connectMs });
|
|
35
|
+
}
|
|
36
|
+
failover(from, to) {
|
|
37
|
+
this.failovers++;
|
|
38
|
+
this.emit({ kind: "failover", from, to });
|
|
39
|
+
}
|
|
40
|
+
relayMessage(bytes) {
|
|
41
|
+
this.relayMessages++;
|
|
42
|
+
this.relayBytes += bytes;
|
|
43
|
+
this.emit({ kind: "relay_message", bytes });
|
|
44
|
+
}
|
|
45
|
+
handshakeFailure(cause) {
|
|
46
|
+
this.handshakeFailures[cause] = (this.handshakeFailures[cause] ?? 0) + 1;
|
|
47
|
+
this.emit({ kind: "handshake_failure", cause });
|
|
48
|
+
}
|
|
49
|
+
reconnect(cause) {
|
|
50
|
+
this.reconnects[cause] = (this.reconnects[cause] ?? 0) + 1;
|
|
51
|
+
this.emit({ kind: "reconnect", cause });
|
|
52
|
+
}
|
|
53
|
+
snapshot() {
|
|
54
|
+
return {
|
|
55
|
+
sessionsByTransport: { ...this.sessionsByTransport },
|
|
56
|
+
failovers: this.failovers,
|
|
57
|
+
timeToConnectMs: Object.fromEntries(Object.entries(this.timeToConnect).map(([k, v]) => [k, { ...v }])),
|
|
58
|
+
relay: { messages: this.relayMessages, bytes: this.relayBytes },
|
|
59
|
+
handshakeFailures: { ...this.handshakeFailures },
|
|
60
|
+
reconnects: { ...this.reconnects },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=telemetry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"telemetry.js","sourceRoot":"","sources":["../../src/telemetry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAiCH,MAAM,OAAO,SAAS;IACZ,mBAAmB,GAA2B,EAAE,CAAC;IACjD,SAAS,GAAG,CAAC,CAAC;IACd,aAAa,GAAgC,EAAE,CAAC;IAChD,aAAa,GAAG,CAAC,CAAC;IAClB,UAAU,GAAG,CAAC,CAAC;IACf,iBAAiB,GAA2B,EAAE,CAAC;IAC/C,UAAU,GAA2B,EAAE,CAAC;IAEhD,qEAAqE;IACrE,OAAO,CAAmC;IAElC,IAAI,CAAC,KAAqB;QAChC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;IACxB,CAAC;IAED,kBAAkB,CAAC,SAAwB,EAAE,SAAiB;QAC5D,IAAI,CAAC,mBAAmB,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,mBAAmB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACrF,MAAM,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QAC9E,CAAC,CAAC,KAAK,EAAE,CAAC;QACV,CAAC,CAAC,OAAO,IAAI,SAAS,CAAC;QACvB,CAAC,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QACvC,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAClC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,CAAC,CAAC;IACvD,CAAC;IAED,QAAQ,CAAC,IAAmB,EAAE,EAAiB;QAC7C,IAAI,CAAC,SAAS,EAAE,CAAC;QACjB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAC5C,CAAC;IAED,YAAY,CAAC,KAAa;QACxB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,UAAU,IAAI,KAAK,CAAC;QACzB,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,CAAC,CAAC;IAC9C,CAAC;IAED,gBAAgB,CAAC,KAA4B;QAC3C,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QACzE,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,KAAK,EAAE,CAAC,CAAC;IAClD,CAAC;IAED,SAAS,CAAC,KAAqB;QAC7B,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;QAC3D,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,CAAC,CAAC;IAC1C,CAAC;IAED,QAAQ;QACN,OAAO;YACL,mBAAmB,EAAE,EAAE,GAAG,IAAI,CAAC,mBAAmB,EAAE;YACpD,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,eAAe,EAAE,MAAM,CAAC,WAAW,CACjC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC,CAClE;YACD,KAAK,EAAE,EAAE,QAAQ,EAAE,IAAI,CAAC,aAAa,EAAE,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE;YAC/D,iBAAiB,EAAE,EAAE,GAAG,IAAI,CAAC,iBAAiB,EAAE;YAChD,UAAU,EAAE,EAAE,GAAG,IAAI,CAAC,UAAU,EAAE;SACnC,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport abstraction + channel mux (docs/WEBRTC_MIGRATION.md §3.7, Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* `Transport` is the thin byte-pipe every path implements — loopback (this
|
|
5
|
+
* file), LAN TCP (Phase 3), the DO relay (Phase 4), and WebRTC (Phase 6). It
|
|
6
|
+
* moves opaque frame bytes and exposes a `bufferedAmount` so the sender can
|
|
7
|
+
* apply backpressure. `Mux` layers the frame codec (framing.ts) on top: it
|
|
8
|
+
* chunks large messages, multiplexes the control/rpc/terminal/events channels
|
|
9
|
+
* over the one pipe, reassembles on receipt, and pauses the sender above a
|
|
10
|
+
* high-water mark so a terminal firehose can't queue multi-second lag.
|
|
11
|
+
*/
|
|
12
|
+
export interface Transport {
|
|
13
|
+
/** Send one already-encoded frame. */
|
|
14
|
+
send(bytes: Buffer): void;
|
|
15
|
+
/** Bytes handed to the transport but not yet flushed to the peer. */
|
|
16
|
+
bufferedAmount(): number;
|
|
17
|
+
/** Register the inbound-frame handler (one per transport). */
|
|
18
|
+
onFrame(cb: (bytes: Buffer) => void): void;
|
|
19
|
+
/** Register a close handler. */
|
|
20
|
+
onClose(cb: () => void): void;
|
|
21
|
+
/**
|
|
22
|
+
* Resolve once `bufferedAmount() < highWaterMark`. The sender awaits this
|
|
23
|
+
* between chunks to bound queue depth. Resolves immediately if already below.
|
|
24
|
+
*/
|
|
25
|
+
whenWritable(highWaterMark: number): Promise<void>;
|
|
26
|
+
close(): void;
|
|
27
|
+
}
|
|
28
|
+
/** Default backpressure high-water mark (1 MiB queued). */
|
|
29
|
+
export declare const DEFAULT_HIGH_WATER: number;
|
|
30
|
+
/**
|
|
31
|
+
* An in-process, ordered, reliable transport pair for tests and same-process
|
|
32
|
+
* wiring. `pair()` returns two endpoints; a frame sent on one is delivered to
|
|
33
|
+
* the other on a later microtask (so `bufferedAmount` is observable and drains
|
|
34
|
+
* asynchronously, exercising the backpressure path).
|
|
35
|
+
*/
|
|
36
|
+
export declare class LoopbackTransport implements Transport {
|
|
37
|
+
private peer;
|
|
38
|
+
private frameCb;
|
|
39
|
+
private closeCb;
|
|
40
|
+
private buffered;
|
|
41
|
+
private closed;
|
|
42
|
+
private drainWaiters;
|
|
43
|
+
static pair(): [LoopbackTransport, LoopbackTransport];
|
|
44
|
+
send(bytes: Buffer): void;
|
|
45
|
+
bufferedAmount(): number;
|
|
46
|
+
onFrame(cb: (bytes: Buffer) => void): void;
|
|
47
|
+
onClose(cb: () => void): void;
|
|
48
|
+
whenWritable(highWaterMark: number): Promise<void>;
|
|
49
|
+
private wakeDrainWaiters;
|
|
50
|
+
close(): void;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Channel multiplexer over a `Transport`. Chunks + reassembles messages, keeps a
|
|
54
|
+
* per-channel outgoing sequence, and applies backpressure between chunks.
|
|
55
|
+
*/
|
|
56
|
+
export declare class Mux {
|
|
57
|
+
private readonly transport;
|
|
58
|
+
private readonly highWaterMark;
|
|
59
|
+
private readonly reassembler;
|
|
60
|
+
private readonly seqs;
|
|
61
|
+
private onMsg;
|
|
62
|
+
constructor(transport: Transport, opts?: {
|
|
63
|
+
highWaterMark?: number;
|
|
64
|
+
});
|
|
65
|
+
/** Register the reassembled-message handler. */
|
|
66
|
+
onMessage(cb: (channelId: number, message: Buffer) => void): void;
|
|
67
|
+
/** True while the transport buffer is below the high-water mark. */
|
|
68
|
+
writable(): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Send a logical message on `channelId`, chunked as needed. Resolves once all
|
|
71
|
+
* chunks are handed to the transport; awaits drain between chunks so a large
|
|
72
|
+
* message (or a terminal firehose) can't grow the queue without bound.
|
|
73
|
+
*/
|
|
74
|
+
send(channelId: number, message: Buffer): Promise<void>;
|
|
75
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transport abstraction + channel mux (docs/WEBRTC_MIGRATION.md §3.7, Phase 2).
|
|
3
|
+
*
|
|
4
|
+
* `Transport` is the thin byte-pipe every path implements — loopback (this
|
|
5
|
+
* file), LAN TCP (Phase 3), the DO relay (Phase 4), and WebRTC (Phase 6). It
|
|
6
|
+
* moves opaque frame bytes and exposes a `bufferedAmount` so the sender can
|
|
7
|
+
* apply backpressure. `Mux` layers the frame codec (framing.ts) on top: it
|
|
8
|
+
* chunks large messages, multiplexes the control/rpc/terminal/events channels
|
|
9
|
+
* over the one pipe, reassembles on receipt, and pauses the sender above a
|
|
10
|
+
* high-water mark so a terminal firehose can't queue multi-second lag.
|
|
11
|
+
*/
|
|
12
|
+
import { decodeFrame, encodeFrame, splitMessage, Reassembler, } from "./framing.js";
|
|
13
|
+
/** Default backpressure high-water mark (1 MiB queued). */
|
|
14
|
+
export const DEFAULT_HIGH_WATER = 1024 * 1024;
|
|
15
|
+
/**
|
|
16
|
+
* An in-process, ordered, reliable transport pair for tests and same-process
|
|
17
|
+
* wiring. `pair()` returns two endpoints; a frame sent on one is delivered to
|
|
18
|
+
* the other on a later microtask (so `bufferedAmount` is observable and drains
|
|
19
|
+
* asynchronously, exercising the backpressure path).
|
|
20
|
+
*/
|
|
21
|
+
export class LoopbackTransport {
|
|
22
|
+
peer;
|
|
23
|
+
frameCb = null;
|
|
24
|
+
closeCb = null;
|
|
25
|
+
buffered = 0;
|
|
26
|
+
closed = false;
|
|
27
|
+
drainWaiters = [];
|
|
28
|
+
static pair() {
|
|
29
|
+
const a = new LoopbackTransport();
|
|
30
|
+
const b = new LoopbackTransport();
|
|
31
|
+
a.peer = b;
|
|
32
|
+
b.peer = a;
|
|
33
|
+
return [a, b];
|
|
34
|
+
}
|
|
35
|
+
send(bytes) {
|
|
36
|
+
if (this.closed)
|
|
37
|
+
return;
|
|
38
|
+
const copy = Buffer.from(bytes);
|
|
39
|
+
this.buffered += copy.length;
|
|
40
|
+
// Deliver asynchronously, then decrement our buffer and wake drain waiters.
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
this.peer.frameCb?.(copy);
|
|
43
|
+
this.buffered -= copy.length;
|
|
44
|
+
this.wakeDrainWaiters();
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
bufferedAmount() {
|
|
48
|
+
return this.buffered;
|
|
49
|
+
}
|
|
50
|
+
onFrame(cb) {
|
|
51
|
+
this.frameCb = cb;
|
|
52
|
+
}
|
|
53
|
+
onClose(cb) {
|
|
54
|
+
this.closeCb = cb;
|
|
55
|
+
}
|
|
56
|
+
whenWritable(highWaterMark) {
|
|
57
|
+
if (this.buffered < highWaterMark)
|
|
58
|
+
return Promise.resolve();
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
this.drainWaiters.push({ hw: highWaterMark, resolve });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
wakeDrainWaiters() {
|
|
64
|
+
if (this.drainWaiters.length === 0)
|
|
65
|
+
return;
|
|
66
|
+
const still = [];
|
|
67
|
+
for (const w of this.drainWaiters) {
|
|
68
|
+
if (this.buffered < w.hw)
|
|
69
|
+
w.resolve();
|
|
70
|
+
else
|
|
71
|
+
still.push(w);
|
|
72
|
+
}
|
|
73
|
+
this.drainWaiters = still;
|
|
74
|
+
}
|
|
75
|
+
close() {
|
|
76
|
+
if (this.closed)
|
|
77
|
+
return;
|
|
78
|
+
this.closed = true;
|
|
79
|
+
for (const w of this.drainWaiters)
|
|
80
|
+
w.resolve();
|
|
81
|
+
this.drainWaiters = [];
|
|
82
|
+
queueMicrotask(() => this.peer.closeCb?.());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Channel multiplexer over a `Transport`. Chunks + reassembles messages, keeps a
|
|
87
|
+
* per-channel outgoing sequence, and applies backpressure between chunks.
|
|
88
|
+
*/
|
|
89
|
+
export class Mux {
|
|
90
|
+
transport;
|
|
91
|
+
highWaterMark;
|
|
92
|
+
reassembler = new Reassembler();
|
|
93
|
+
seqs = new Map();
|
|
94
|
+
onMsg = null;
|
|
95
|
+
constructor(transport, opts) {
|
|
96
|
+
this.transport = transport;
|
|
97
|
+
this.highWaterMark = opts?.highWaterMark ?? DEFAULT_HIGH_WATER;
|
|
98
|
+
this.transport.onFrame((bytes) => {
|
|
99
|
+
let out;
|
|
100
|
+
try {
|
|
101
|
+
out = this.reassembler.push(decodeFrame(bytes));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return; // malformed/oversized frame — drop, keep the transport
|
|
105
|
+
}
|
|
106
|
+
if (out)
|
|
107
|
+
this.onMsg?.(out.channelId, out.message);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** Register the reassembled-message handler. */
|
|
111
|
+
onMessage(cb) {
|
|
112
|
+
this.onMsg = cb;
|
|
113
|
+
}
|
|
114
|
+
/** True while the transport buffer is below the high-water mark. */
|
|
115
|
+
writable() {
|
|
116
|
+
return this.transport.bufferedAmount() < this.highWaterMark;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Send a logical message on `channelId`, chunked as needed. Resolves once all
|
|
120
|
+
* chunks are handed to the transport; awaits drain between chunks so a large
|
|
121
|
+
* message (or a terminal firehose) can't grow the queue without bound.
|
|
122
|
+
*/
|
|
123
|
+
async send(channelId, message) {
|
|
124
|
+
let seq = this.seqs.get(channelId) ?? 0;
|
|
125
|
+
const frames = splitMessage(channelId, seq, message);
|
|
126
|
+
for (const frame of frames) {
|
|
127
|
+
this.transport.send(encodeFrame(frame));
|
|
128
|
+
seq = (frame.seq + 1) >>> 0;
|
|
129
|
+
if (this.transport.bufferedAmount() >= this.highWaterMark) {
|
|
130
|
+
await this.transport.whenWritable(this.highWaterMark);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.seqs.set(channelId, seq);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,WAAW,EACX,WAAW,EACX,YAAY,EACZ,WAAW,GACZ,MAAM,cAAc,CAAC;AAmBtB,2DAA2D;AAC3D,MAAM,CAAC,MAAM,kBAAkB,GAAG,IAAI,GAAG,IAAI,CAAC;AAE9C;;;;;GAKG;AACH,MAAM,OAAO,iBAAiB;IACpB,IAAI,CAAqB;IACzB,OAAO,GAAqC,IAAI,CAAC;IACjD,OAAO,GAAwB,IAAI,CAAC;IACpC,QAAQ,GAAG,CAAC,CAAC;IACb,MAAM,GAAG,KAAK,CAAC;IACf,YAAY,GAA+C,EAAE,CAAC;IAEtE,MAAM,CAAC,IAAI;QACT,MAAM,CAAC,GAAG,IAAI,iBAAiB,EAAE,CAAC;QAClC,MAAM,CAAC,GAAG,IAAI,iBAAiB,EAAE,CAAC;QAClC,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QACX,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC;QACX,OAAO,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IAChB,CAAC;IAED,IAAI,CAAC,KAAa;QAChB,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAChC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC;QAC7B,4EAA4E;QAC5E,cAAc,CAAC,GAAG,EAAE;YAClB,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,CAAC;YAC1B,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC;YAC7B,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,cAAc;QACZ,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,OAAO,CAAC,EAA2B;QACjC,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACpB,CAAC;IAED,OAAO,CAAC,EAAc;QACpB,IAAI,CAAC,OAAO,GAAG,EAAE,CAAC;IACpB,CAAC;IAED,YAAY,CAAC,aAAqB;QAChC,IAAI,IAAI,CAAC,QAAQ,GAAG,aAAa;YAAE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;QAC5D,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;YACnC,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,CAAC,CAAC;QACzD,CAAC,CAAC,CAAC;IACL,CAAC;IAEO,gBAAgB;QACtB,IAAI,IAAI,CAAC,YAAY,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO;QAC3C,MAAM,KAAK,GAA6B,EAAE,CAAC;QAC3C,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YAClC,IAAI,IAAI,CAAC,QAAQ,GAAG,CAAC,CAAC,EAAE;gBAAE,CAAC,CAAC,OAAO,EAAE,CAAC;;gBACjC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACrB,CAAC;QACD,IAAI,CAAC,YAAY,GAAG,KAAK,CAAC;IAC5B,CAAC;IAED,KAAK;QACH,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO;QACxB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,YAAY;YAAE,CAAC,CAAC,OAAO,EAAE,CAAC;QAC/C,IAAI,CAAC,YAAY,GAAG,EAAE,CAAC;QACvB,cAAc,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IAC9C,CAAC;CACF;AAED;;;GAGG;AACH,MAAM,OAAO,GAAG;IAOK;IANF,aAAa,CAAS;IACtB,WAAW,GAAG,IAAI,WAAW,EAAE,CAAC;IAChC,IAAI,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC1C,KAAK,GAA0D,IAAI,CAAC;IAE5E,YACmB,SAAoB,EACrC,IAAiC;QADhB,cAAS,GAAT,SAAS,CAAW;QAGrC,IAAI,CAAC,aAAa,GAAG,IAAI,EAAE,aAAa,IAAI,kBAAkB,CAAC;QAC/D,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;YAC/B,IAAI,GAAkD,CAAC;YACvD,IAAI,CAAC;gBACH,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;YAClD,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,CAAC,uDAAuD;YACjE,CAAC;YACD,IAAI,GAAG;gBAAE,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;IACL,CAAC;IAED,gDAAgD;IAChD,SAAS,CAAC,EAAgD;QACxD,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC;IAClB,CAAC;IAED,oEAAoE;IACpE,QAAQ;QACN,OAAO,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,GAAG,IAAI,CAAC,aAAa,CAAC;IAC9D,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,IAAI,CAAC,SAAiB,EAAE,OAAe;QAC3C,IAAI,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,MAAM,GAAG,YAAY,CAAC,SAAS,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QACrD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,CAAC;YACxC,GAAG,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;YAC5B,IAAI,IAAI,CAAC,SAAS,CAAC,cAAc,EAAE,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;gBAC1D,MAAM,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;IAChC,CAAC;CACF"}
|
package/dist/src/workspace.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* worktree, agent, and terminal. Translates agent events into broadcast wire
|
|
4
4
|
* messages and persists metadata so sessions survive a daemon restart (resume).
|
|
5
5
|
*/
|
|
6
|
-
import type {
|
|
6
|
+
import type { SocketLike } from "./hub.js";
|
|
7
7
|
import type { ChatEntry, DeleteBatchResultPayload, PromptAttachment, QuestionAnswers, WorkspaceActionKind, WorkspaceSummary } from "./protocol.js";
|
|
8
8
|
import type { ProjectRecord, WorkspaceRecord } from "./types.js";
|
|
9
9
|
import type { Hub } from "./hub.js";
|
|
@@ -14,6 +14,7 @@ import { ProjectStore } from "./projects.js";
|
|
|
14
14
|
import { AppConfigStore } from "./config.js";
|
|
15
15
|
import { CredentialsStore } from "./credentials.js";
|
|
16
16
|
import { AllowlistStore } from "./allowlist.js";
|
|
17
|
+
import type { PushNotifier } from "./push.js";
|
|
17
18
|
import type { PermissionResponse } from "./agent/agent.js";
|
|
18
19
|
export declare class WorkspaceManager {
|
|
19
20
|
private hub;
|
|
@@ -47,6 +48,13 @@ export declare class WorkspaceManager {
|
|
|
47
48
|
* a permission request with `allow_all`. Defaulted for existing callers/tests.
|
|
48
49
|
*/
|
|
49
50
|
private allowlist;
|
|
51
|
+
/**
|
|
52
|
+
* FCM remote-wake. Optional so existing callers/tests need no change; when
|
|
53
|
+
* present, the manager wakes phones on a permission request / agent-done /
|
|
54
|
+
* usage limit — but only when no client socket is live (the notifier gates
|
|
55
|
+
* on that), so a foregrounded app is never double-notified. See push.ts.
|
|
56
|
+
*/
|
|
57
|
+
private push?;
|
|
50
58
|
private workspaces;
|
|
51
59
|
/**
|
|
52
60
|
* The last workspace list we fanned out, JSON-encoded — so
|
|
@@ -83,7 +91,14 @@ export declare class WorkspaceManager {
|
|
|
83
91
|
* grant auto-approves matching calls, and appended to when a client answers
|
|
84
92
|
* a permission request with `allow_all`. Defaulted for existing callers/tests.
|
|
85
93
|
*/
|
|
86
|
-
allowlist?: AllowlistStore
|
|
94
|
+
allowlist?: AllowlistStore,
|
|
95
|
+
/**
|
|
96
|
+
* FCM remote-wake. Optional so existing callers/tests need no change; when
|
|
97
|
+
* present, the manager wakes phones on a permission request / agent-done /
|
|
98
|
+
* usage limit — but only when no client socket is live (the notifier gates
|
|
99
|
+
* on that), so a foregrounded app is never double-notified. See push.ts.
|
|
100
|
+
*/
|
|
101
|
+
push?: PushNotifier | undefined);
|
|
87
102
|
/** The daemon-global allowlist (for server-side admin: list/revoke/clear). */
|
|
88
103
|
allowlistStore(): AllowlistStore;
|
|
89
104
|
/** Is there a live workspace with this id? (used by the job scheduler). */
|
|
@@ -327,10 +342,10 @@ export declare class WorkspaceManager {
|
|
|
327
342
|
* are intentionally left intact so their scrollback survives a reconnect.
|
|
328
343
|
*/
|
|
329
344
|
reapDevice(deviceId: string): void;
|
|
330
|
-
terminalInput(id: string, deviceId: string, data: string, socket:
|
|
331
|
-
terminalResize(id: string, deviceId: string, cols: number, rows: number, socket:
|
|
345
|
+
terminalInput(id: string, deviceId: string, data: string, socket: SocketLike): void;
|
|
346
|
+
terminalResize(id: string, deviceId: string, cols: number, rows: number, socket: SocketLike): void;
|
|
332
347
|
/** This device's retained PTY scrollback ("" until it has started one). */
|
|
333
|
-
terminalSnapshot(id: string, deviceId: string, socket:
|
|
348
|
+
terminalSnapshot(id: string, deviceId: string, socket: SocketLike): string | null;
|
|
334
349
|
gitStatus(id: string, sync?: boolean): Promise<import("./protocol.js").GitStatusResultPayload | null>;
|
|
335
350
|
gitCommit(id: string, message: string): Promise<import("./protocol.js").GitResultPayload | null>;
|
|
336
351
|
gitPush(id: string): Promise<import("./protocol.js").GitResultPayload | null>;
|
package/dist/src/workspace.js
CHANGED
|
@@ -83,6 +83,7 @@ export class WorkspaceManager {
|
|
|
83
83
|
credentials;
|
|
84
84
|
appConfig;
|
|
85
85
|
allowlist;
|
|
86
|
+
push;
|
|
86
87
|
workspaces = new Map();
|
|
87
88
|
/**
|
|
88
89
|
* The last workspace list we fanned out, JSON-encoded — so
|
|
@@ -119,7 +120,14 @@ export class WorkspaceManager {
|
|
|
119
120
|
* grant auto-approves matching calls, and appended to when a client answers
|
|
120
121
|
* a permission request with `allow_all`. Defaulted for existing callers/tests.
|
|
121
122
|
*/
|
|
122
|
-
allowlist = new AllowlistStore()
|
|
123
|
+
allowlist = new AllowlistStore(),
|
|
124
|
+
/**
|
|
125
|
+
* FCM remote-wake. Optional so existing callers/tests need no change; when
|
|
126
|
+
* present, the manager wakes phones on a permission request / agent-done /
|
|
127
|
+
* usage limit — but only when no client socket is live (the notifier gates
|
|
128
|
+
* on that), so a foregrounded app is never double-notified. See push.ts.
|
|
129
|
+
*/
|
|
130
|
+
push) {
|
|
123
131
|
this.hub = hub;
|
|
124
132
|
this.chatlog = chatlog;
|
|
125
133
|
this.media = media;
|
|
@@ -128,6 +136,7 @@ export class WorkspaceManager {
|
|
|
128
136
|
this.credentials = credentials;
|
|
129
137
|
this.appConfig = appConfig;
|
|
130
138
|
this.allowlist = allowlist;
|
|
139
|
+
this.push = push;
|
|
131
140
|
}
|
|
132
141
|
/** The daemon-global allowlist (for server-side admin: list/revoke/clear). */
|
|
133
142
|
allowlistStore() {
|
|
@@ -1263,6 +1272,9 @@ export class WorkspaceManager {
|
|
|
1263
1272
|
inputSummary: e.inputSummary,
|
|
1264
1273
|
seq: permEntry.seq,
|
|
1265
1274
|
});
|
|
1275
|
+
// Remote-wake a backgrounded phone so the deferred call doesn't sit
|
|
1276
|
+
// unseen (no-op when a client socket is live — see PushNotifier).
|
|
1277
|
+
this.push?.notifyPermission(id, live.record.name, e.tool, e.inputSummary, permEntry.seq);
|
|
1266
1278
|
break;
|
|
1267
1279
|
}
|
|
1268
1280
|
case "permission_resolved":
|
|
@@ -1327,6 +1339,9 @@ export class WorkspaceManager {
|
|
|
1327
1339
|
usageText,
|
|
1328
1340
|
seq: doneEntry.seq,
|
|
1329
1341
|
});
|
|
1342
|
+
// Remote-wake so a turn that finished while the app was closed still
|
|
1343
|
+
// notifies (no-op when a client socket is live — see PushNotifier).
|
|
1344
|
+
this.push?.notifyDone(id, live.record.name, e.summary, doneEntry.seq);
|
|
1330
1345
|
// The turn almost certainly touched files, so the sidebar diff-stat is
|
|
1331
1346
|
// now stale — recompute and fan the fresh counts out (no-ops if the turn
|
|
1332
1347
|
// changed nothing). This keeps a background workspace's counter live
|
|
@@ -1349,6 +1364,9 @@ export class WorkspaceManager {
|
|
|
1349
1364
|
message: e.message,
|
|
1350
1365
|
resetAt: e.resetAt,
|
|
1351
1366
|
});
|
|
1367
|
+
// Remote-wake so the user learns the run stalled on a limit even with the
|
|
1368
|
+
// app closed (no-op when a client socket is live — see PushNotifier).
|
|
1369
|
+
this.push?.notifyLimit(id, live.record.name, e.message);
|
|
1352
1370
|
// Persist as a `limit` entry (not a plain system line) so a reconnecting
|
|
1353
1371
|
// client replays the actionable restart card, not just static text.
|
|
1354
1372
|
void this.chatlog.append(id, {
|