wicked-crew 0.1.0 → 0.2.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.
@@ -0,0 +1,106 @@
1
+ import { WebSocket } from 'ws';
2
+ /**
3
+ * Per-terminal WS fan-out (DES-TERMINAL-001 §6). The daemon's single CoreEvent
4
+ * subscription is the source of truth; this hub routes the `terminalOutput` /
5
+ * `terminalExited` frames for a given terminal id to the ONE browser socket that
6
+ * opened it, keyed by id (a `Map<id, socket>`). Raw PTY output arrives
7
+ * base64-encoded (`bytesB64`); we decode and send it as a BINARY frame so
8
+ * xterm.js writes the exact bytes — control sequences and multi-byte UTF-8
9
+ * survive a text-frame round-trip.
10
+ */
11
+ export class TerminalHub {
12
+ sockets = new Map();
13
+ /**
14
+ * ids whose `terminalExited` we've already routed — so the WS close handler
15
+ * skips a redundant `closeTerminal` (the engine already reaped that child).
16
+ */
17
+ exited = new Set();
18
+ /** Bind a browser socket to a terminal id (later `terminalOutput` fans out to it). */
19
+ register(id, socket) {
20
+ this.sockets.set(id, socket);
21
+ }
22
+ /** True once a `terminalExited` frame has been routed for this id. */
23
+ hasExited(id) {
24
+ return this.exited.has(id);
25
+ }
26
+ /** Drop the socket + exit bookkeeping for a terminal id. */
27
+ unregister(id) {
28
+ this.sockets.delete(id);
29
+ this.exited.delete(id);
30
+ }
31
+ /** Live terminal-socket count (diagnostics / tests). */
32
+ size() {
33
+ return this.sockets.size;
34
+ }
35
+ /**
36
+ * Route one CoreEvent frame to the terminal socket it belongs to. Called for
37
+ * EVERY core event; non-terminal frames and frames for an unknown id are no-ops.
38
+ */
39
+ route(event) {
40
+ const id = typeof event.id === 'string' ? event.id : undefined;
41
+ if (id === undefined)
42
+ return;
43
+ const socket = this.sockets.get(id);
44
+ if (!socket)
45
+ return;
46
+ if (event.type === 'terminalOutput') {
47
+ if (typeof event.bytesB64 === 'string' && socket.readyState === WebSocket.OPEN) {
48
+ socket.send(Buffer.from(event.bytesB64, 'base64'));
49
+ }
50
+ }
51
+ else if (event.type === 'terminalExited') {
52
+ // The child is gone. Mark it (so the socket-close handler skips a redundant
53
+ // closeTerminal), then close the browser socket so xterm stops cleanly.
54
+ this.exited.add(id);
55
+ if (socket.readyState === WebSocket.OPEN)
56
+ socket.close();
57
+ }
58
+ }
59
+ }
60
+ /** Normalise a ws frame (Buffer | ArrayBuffer | Buffer[]) to a single Buffer. */
61
+ function toBuffer(data) {
62
+ if (Buffer.isBuffer(data))
63
+ return data;
64
+ if (Array.isArray(data))
65
+ return Buffer.concat(data);
66
+ return Buffer.from(data);
67
+ }
68
+ /**
69
+ * The per-terminal WS channel: `GET /ws/terminals/:id`. Binds the browser socket
70
+ * to the terminal id in the hub (so `terminalOutput` for that id fans out to it),
71
+ * forwards every inbound browser frame to the PTY as raw stdin bytes, and closes
72
+ * the terminal when the socket drops (unless the PTY already exited on its own).
73
+ */
74
+ export function registerTerminalWs(app, adapter, hub) {
75
+ app.get('/ws/terminals/:id', { websocket: true }, (socket, req) => {
76
+ const { id } = req.params;
77
+ const ws = socket;
78
+ hub.register(id, ws);
79
+ // Browser keystrokes (xterm.onData → ws.send): text or binary, both are raw
80
+ // input bytes for the PTY. Failures (unknown/closed id) are swallowed — the
81
+ // exit/close path tears the socket down.
82
+ ws.on('message', (data) => {
83
+ void adapter.writeTerminal(id, toBuffer(data)).catch(() => {
84
+ /* terminal gone */
85
+ });
86
+ });
87
+ let torn = false;
88
+ const teardown = () => {
89
+ if (torn)
90
+ return;
91
+ torn = true;
92
+ const alreadyExited = hub.hasExited(id);
93
+ hub.unregister(id);
94
+ // Socket dropped by the browser (unmount) → close the PTY. Skip when the PTY
95
+ // already exited (engine reaped it; a second closeTerminal would reject).
96
+ if (!alreadyExited) {
97
+ void adapter.closeTerminal(id).catch(() => {
98
+ /* already closed / unknown id */
99
+ });
100
+ }
101
+ };
102
+ ws.on('close', teardown);
103
+ ws.on('error', teardown);
104
+ });
105
+ }
106
+ //# sourceMappingURL=terminals.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"terminals.js","sourceRoot":"","sources":["../../src/events/terminals.ts"],"names":[],"mappings":"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"}