@slopus/ghostty-web 0.1.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.
Files changed (39) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +85 -0
  3. package/dist/GhosttyRemoteTerminal.d.ts +69 -0
  4. package/dist/GhosttyRemoteTerminal.d.ts.map +1 -0
  5. package/dist/GhosttyRemoteTerminal.js +212 -0
  6. package/dist/RemoteTerminalProtocolClient.d.ts +30 -0
  7. package/dist/RemoteTerminalProtocolClient.d.ts.map +1 -0
  8. package/dist/RemoteTerminalProtocolClient.js +350 -0
  9. package/dist/RemoteTerminalProtocolServer.d.ts +75 -0
  10. package/dist/RemoteTerminalProtocolServer.d.ts.map +1 -0
  11. package/dist/RemoteTerminalProtocolServer.js +794 -0
  12. package/dist/WirePacket.d.ts +29 -0
  13. package/dist/WirePacket.d.ts.map +1 -0
  14. package/dist/WirePacket.js +24 -0
  15. package/dist/WirePacketDecoder.d.ts +8 -0
  16. package/dist/WirePacketDecoder.d.ts.map +1 -0
  17. package/dist/WirePacketDecoder.js +130 -0
  18. package/dist/applyGridPatch.d.ts +3 -0
  19. package/dist/applyGridPatch.d.ts.map +1 -0
  20. package/dist/applyGridPatch.js +19 -0
  21. package/dist/diffGridState.d.ts +3 -0
  22. package/dist/diffGridState.d.ts.map +1 -0
  23. package/dist/diffGridState.js +26 -0
  24. package/dist/encodeWirePacket.d.ts +7 -0
  25. package/dist/encodeWirePacket.d.ts.map +1 -0
  26. package/dist/encodeWirePacket.js +25 -0
  27. package/dist/index.d.ts +13 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +9 -0
  30. package/dist/jsonPayload.d.ts +3 -0
  31. package/dist/jsonPayload.d.ts.map +1 -0
  32. package/dist/jsonPayload.js +8 -0
  33. package/dist/testing/ThrottledTcpProxy.d.ts +15 -0
  34. package/dist/testing/ThrottledTcpProxy.d.ts.map +1 -0
  35. package/dist/testing/ThrottledTcpProxy.js +83 -0
  36. package/dist/types.d.ts +105 -0
  37. package/dist/types.d.ts.map +1 -0
  38. package/dist/types.js +1 -0
  39. package/package.json +32 -0
@@ -0,0 +1,29 @@
1
+ export declare const WIRE_VERSION = 1;
2
+ export declare const WIRE_HEADER_BYTES = 20;
3
+ export declare const WIRE_MAGIC = 21076;
4
+ export declare const enum WirePacketType {
5
+ ClientHello = 1,
6
+ Welcome = 2,
7
+ Output = 3,
8
+ OutputAck = 4,
9
+ Input = 5,
10
+ InputAck = 6,
11
+ Resize = 7,
12
+ ResizeAck = 8,
13
+ GridKeyframe = 9,
14
+ GridPatch = 10,
15
+ GridAck = 11,
16
+ Mode = 12,
17
+ Resync = 13,
18
+ ScrollbackRequest = 14,
19
+ ScrollbackPage = 15,
20
+ Exit = 16,
21
+ Error = 17,
22
+ ResizeApplied = 18
23
+ }
24
+ export interface WirePacket {
25
+ payload: Uint8Array;
26
+ sequence: number;
27
+ type: WirePacketType;
28
+ }
29
+ //# sourceMappingURL=WirePacket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WirePacket.d.ts","sourceRoot":"","sources":["../sources/WirePacket.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,YAAY,IAAI,CAAC;AAC9B,eAAO,MAAM,iBAAiB,KAAK,CAAC;AACpC,eAAO,MAAM,UAAU,QAAS,CAAC;AAEjC,0BAAkB,cAAc;IAC5B,WAAW,IAAI;IACf,OAAO,IAAI;IACX,MAAM,IAAI;IACV,SAAS,IAAI;IACb,KAAK,IAAI;IACT,QAAQ,IAAI;IACZ,MAAM,IAAI;IACV,SAAS,IAAI;IACb,YAAY,IAAI;IAChB,SAAS,KAAK;IACd,OAAO,KAAK;IACZ,IAAI,KAAK;IACT,MAAM,KAAK;IACX,iBAAiB,KAAK;IACtB,cAAc,KAAK;IACnB,IAAI,KAAK;IACT,KAAK,KAAK;IACV,aAAa,KAAK;CACrB;AAED,MAAM,WAAW,UAAU;IACvB,OAAO,EAAE,UAAU,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,cAAc,CAAC;CACxB"}
@@ -0,0 +1,24 @@
1
+ export const WIRE_VERSION = 1;
2
+ export const WIRE_HEADER_BYTES = 20;
3
+ export const WIRE_MAGIC = 0x5254;
4
+ export var WirePacketType;
5
+ (function (WirePacketType) {
6
+ WirePacketType[WirePacketType["ClientHello"] = 1] = "ClientHello";
7
+ WirePacketType[WirePacketType["Welcome"] = 2] = "Welcome";
8
+ WirePacketType[WirePacketType["Output"] = 3] = "Output";
9
+ WirePacketType[WirePacketType["OutputAck"] = 4] = "OutputAck";
10
+ WirePacketType[WirePacketType["Input"] = 5] = "Input";
11
+ WirePacketType[WirePacketType["InputAck"] = 6] = "InputAck";
12
+ WirePacketType[WirePacketType["Resize"] = 7] = "Resize";
13
+ WirePacketType[WirePacketType["ResizeAck"] = 8] = "ResizeAck";
14
+ WirePacketType[WirePacketType["GridKeyframe"] = 9] = "GridKeyframe";
15
+ WirePacketType[WirePacketType["GridPatch"] = 10] = "GridPatch";
16
+ WirePacketType[WirePacketType["GridAck"] = 11] = "GridAck";
17
+ WirePacketType[WirePacketType["Mode"] = 12] = "Mode";
18
+ WirePacketType[WirePacketType["Resync"] = 13] = "Resync";
19
+ WirePacketType[WirePacketType["ScrollbackRequest"] = 14] = "ScrollbackRequest";
20
+ WirePacketType[WirePacketType["ScrollbackPage"] = 15] = "ScrollbackPage";
21
+ WirePacketType[WirePacketType["Exit"] = 16] = "Exit";
22
+ WirePacketType[WirePacketType["Error"] = 17] = "Error";
23
+ WirePacketType[WirePacketType["ResizeApplied"] = 18] = "ResizeApplied";
24
+ })(WirePacketType || (WirePacketType = {}));
@@ -0,0 +1,8 @@
1
+ import { type WirePacket } from "./WirePacket.js";
2
+ /** Incremental bounded decoder. It never recopies the unconsumed byte stream. */
3
+ export declare class WirePacketDecoder {
4
+ #private;
5
+ constructor(maxFrameBytes?: number, maxPacketsPerPush?: number);
6
+ push(data: Uint8Array): readonly WirePacket[];
7
+ }
8
+ //# sourceMappingURL=WirePacketDecoder.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WirePacketDecoder.d.ts","sourceRoot":"","sources":["../sources/WirePacketDecoder.ts"],"names":[],"mappings":"AAEA,OAAO,EAIH,KAAK,UAAU,EAElB,MAAM,iBAAiB,CAAC;AAKzB,iFAAiF;AACjF,qBAAa,iBAAiB;;gBAMd,aAAa,SAAmB,EAAE,iBAAiB,SAAQ;IAmBvE,IAAI,CAAC,IAAI,EAAE,UAAU,GAAG,SAAS,UAAU,EAAE;CAoGhD"}
@@ -0,0 +1,130 @@
1
+ import { inflateRawSync } from "node:zlib";
2
+ import { WIRE_HEADER_BYTES, WIRE_MAGIC, WIRE_VERSION, WirePacketType, } from "./WirePacket.js";
3
+ const COMPRESSED = 1;
4
+ const KNOWN_FLAGS = COMPRESSED;
5
+ /** Incremental bounded decoder. It never recopies the unconsumed byte stream. */
6
+ export class WirePacketDecoder {
7
+ #bufferedBytes = 0;
8
+ #chunks = [];
9
+ #maxFrameBytes;
10
+ #maxPacketsPerPush;
11
+ constructor(maxFrameBytes = 32 * 1024 * 1024, maxPacketsPerPush = 4_096) {
12
+ if (!Number.isSafeInteger(maxFrameBytes) ||
13
+ maxFrameBytes < 1 ||
14
+ maxFrameBytes > 64 * 1024 * 1024) {
15
+ throw new Error("Invalid maximum remote terminal frame size.");
16
+ }
17
+ if (!Number.isSafeInteger(maxPacketsPerPush) ||
18
+ maxPacketsPerPush < 1 ||
19
+ maxPacketsPerPush > 65_536) {
20
+ throw new Error("Invalid remote terminal packet batch limit.");
21
+ }
22
+ this.#maxFrameBytes = maxFrameBytes;
23
+ this.#maxPacketsPerPush = maxPacketsPerPush;
24
+ }
25
+ push(data) {
26
+ if (data.length > 0) {
27
+ this.#chunks.push(Buffer.from(data));
28
+ this.#bufferedBytes += data.length;
29
+ }
30
+ if (this.#bufferedBytes > this.#maxFrameBytes + WIRE_HEADER_BYTES) {
31
+ throw new Error("Remote terminal receive buffer is too large.");
32
+ }
33
+ const packets = [];
34
+ while (this.#bufferedBytes >= WIRE_HEADER_BYTES) {
35
+ if (packets.length >= this.#maxPacketsPerPush) {
36
+ throw new Error("Too many remote terminal packets in one read.");
37
+ }
38
+ const header = this.#peek(WIRE_HEADER_BYTES);
39
+ if (header.readUInt16BE(0) !== WIRE_MAGIC)
40
+ throw new Error("Invalid wire magic.");
41
+ if (header.readUInt8(2) !== WIRE_VERSION) {
42
+ throw new Error("Unsupported remote terminal wire version.");
43
+ }
44
+ const type = header.readUInt8(3);
45
+ if (type < WirePacketType.ClientHello || type > WirePacketType.ResizeApplied) {
46
+ throw new Error("Unknown remote terminal packet type.");
47
+ }
48
+ const flags = header.readUInt8(4);
49
+ if ((flags & ~KNOWN_FLAGS) !== 0)
50
+ throw new Error("Unknown remote terminal packet flags.");
51
+ if (header.readUIntBE(5, 3) !== 0)
52
+ throw new Error("Nonzero reserved wire header bytes.");
53
+ const length = header.readUInt32BE(16);
54
+ if (length > this.#maxFrameBytes)
55
+ throw new Error("Remote terminal frame is too large.");
56
+ const frameLength = WIRE_HEADER_BYTES + length;
57
+ if (this.#bufferedBytes < frameLength)
58
+ break;
59
+ this.#consume(WIRE_HEADER_BYTES);
60
+ const encoded = this.#read(length);
61
+ let payload;
62
+ try {
63
+ payload =
64
+ flags & COMPRESSED
65
+ ? inflateRawSync(encoded, { maxOutputLength: this.#maxFrameBytes })
66
+ : encoded;
67
+ }
68
+ catch (error) {
69
+ throw new Error("Invalid compressed remote terminal frame.", { cause: error });
70
+ }
71
+ if (payload.length > this.#maxFrameBytes) {
72
+ throw new Error("Inflated remote terminal frame is too large.");
73
+ }
74
+ const sequence = Number(header.readBigUInt64BE(8));
75
+ if (!Number.isSafeInteger(sequence))
76
+ throw new Error("Wire sequence is too large.");
77
+ packets.push({ payload, sequence, type: type });
78
+ }
79
+ return packets;
80
+ }
81
+ #consume(length) {
82
+ let remaining = length;
83
+ while (remaining > 0) {
84
+ const first = this.#chunks[0];
85
+ if (first.length <= remaining) {
86
+ this.#chunks.shift();
87
+ remaining -= first.length;
88
+ }
89
+ else {
90
+ this.#chunks[0] = first.subarray(remaining);
91
+ remaining = 0;
92
+ }
93
+ }
94
+ this.#bufferedBytes -= length;
95
+ }
96
+ #peek(length) {
97
+ const first = this.#chunks[0];
98
+ if (first.length >= length)
99
+ return first.subarray(0, length);
100
+ const result = Buffer.allocUnsafe(length);
101
+ let offset = 0;
102
+ for (const chunk of this.#chunks) {
103
+ const copied = Math.min(chunk.length, length - offset);
104
+ chunk.copy(result, offset, 0, copied);
105
+ offset += copied;
106
+ if (offset === length)
107
+ break;
108
+ }
109
+ return result;
110
+ }
111
+ #read(length) {
112
+ if (length === 0)
113
+ return Buffer.alloc(0);
114
+ const first = this.#chunks[0];
115
+ if (first.length === length) {
116
+ this.#chunks.shift();
117
+ this.#bufferedBytes -= length;
118
+ return first;
119
+ }
120
+ if (first.length > length) {
121
+ const result = first.subarray(0, length);
122
+ this.#chunks[0] = first.subarray(length);
123
+ this.#bufferedBytes -= length;
124
+ return result;
125
+ }
126
+ const result = this.#peek(length);
127
+ this.#consume(length);
128
+ return result;
129
+ }
130
+ }
@@ -0,0 +1,3 @@
1
+ import type { RemoteTerminalGridPatch, RemoteTerminalGridState } from "./types.js";
2
+ export declare function applyGridPatch(state: RemoteTerminalGridState, patch: RemoteTerminalGridPatch): RemoteTerminalGridState;
3
+ //# sourceMappingURL=applyGridPatch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"applyGridPatch.d.ts","sourceRoot":"","sources":["../sources/applyGridPatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,wBAAgB,cAAc,CAC1B,KAAK,EAAE,uBAAuB,EAC9B,KAAK,EAAE,uBAAuB,GAC/B,uBAAuB,CAiBzB"}
@@ -0,0 +1,19 @@
1
+ export function applyGridPatch(state, patch) {
2
+ if (state.revision !== patch.baseRevision)
3
+ throw new Error("Grid patch base revision mismatch.");
4
+ const rows = [...state.rows];
5
+ for (const [index, row] of patch.rows)
6
+ rows[index] = row;
7
+ return {
8
+ cols: patch.cols,
9
+ coversOutputOffset: patch.coversOutputOffset,
10
+ cursor: patch.cursor,
11
+ palette: patch.palette,
12
+ revision: patch.revision,
13
+ rows,
14
+ startRow: patch.startRow,
15
+ styles: patch.styles,
16
+ title: patch.title,
17
+ totalRows: patch.totalRows,
18
+ };
19
+ }
@@ -0,0 +1,3 @@
1
+ import type { RemoteTerminalGridPatch, RemoteTerminalGridState } from "./types.js";
2
+ export declare function diffGridState(previous: RemoteTerminalGridState, next: RemoteTerminalGridState): RemoteTerminalGridPatch | undefined;
3
+ //# sourceMappingURL=diffGridState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"diffGridState.d.ts","sourceRoot":"","sources":["../sources/diffGridState.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAEnF,wBAAgB,aAAa,CACzB,QAAQ,EAAE,uBAAuB,EACjC,IAAI,EAAE,uBAAuB,GAC9B,uBAAuB,GAAG,SAAS,CA2BrC"}
@@ -0,0 +1,26 @@
1
+ export function diffGridState(previous, next) {
2
+ if (previous.cols !== next.cols ||
3
+ previous.rows.length !== next.rows.length ||
4
+ previous.startRow !== next.startRow) {
5
+ return undefined;
6
+ }
7
+ const rows = [];
8
+ for (let index = 0; index < next.rows.length; index += 1) {
9
+ if (JSON.stringify(previous.rows[index]) !== JSON.stringify(next.rows[index])) {
10
+ rows.push([index, next.rows[index]]);
11
+ }
12
+ }
13
+ return {
14
+ baseRevision: previous.revision,
15
+ cols: next.cols,
16
+ coversOutputOffset: next.coversOutputOffset,
17
+ cursor: next.cursor,
18
+ palette: next.palette,
19
+ revision: next.revision,
20
+ rows,
21
+ startRow: next.startRow,
22
+ styles: next.styles,
23
+ title: next.title,
24
+ totalRows: next.totalRows,
25
+ };
26
+ }
@@ -0,0 +1,7 @@
1
+ import { type WirePacket } from "./WirePacket.js";
2
+ export declare function encodeWirePacket(packet: WirePacket): {
3
+ compressed: boolean;
4
+ data: Buffer;
5
+ payloadBytes: number;
6
+ };
7
+ //# sourceMappingURL=encodeWirePacket.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"encodeWirePacket.d.ts","sourceRoot":"","sources":["../sources/encodeWirePacket.ts"],"names":[],"mappings":"AAEA,OAAO,EAA+C,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAI/F,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,UAAU,GAAG;IAClD,UAAU,EAAE,OAAO,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;CACxB,CAqBA"}
@@ -0,0 +1,25 @@
1
+ import { deflateRawSync } from "node:zlib";
2
+ import { WIRE_HEADER_BYTES, WIRE_MAGIC, WIRE_VERSION } from "./WirePacket.js";
3
+ const COMPRESSED = 1;
4
+ export function encodeWirePacket(packet) {
5
+ if (!Number.isSafeInteger(packet.sequence) || packet.sequence < 0) {
6
+ throw new Error("A wire sequence must be a non-negative safe integer.");
7
+ }
8
+ if (!Number.isInteger(packet.type) || packet.type < 1 || packet.type > 18) {
9
+ throw new Error("Unknown remote terminal packet type.");
10
+ }
11
+ const source = Buffer.from(packet.payload);
12
+ const compressed = source.length >= 512 ? deflateRawSync(source) : source;
13
+ const useCompression = compressed.length + 16 < source.length;
14
+ const payload = useCompression ? compressed : source;
15
+ const frame = Buffer.allocUnsafe(WIRE_HEADER_BYTES + payload.length);
16
+ frame.writeUInt16BE(WIRE_MAGIC, 0);
17
+ frame.writeUInt8(WIRE_VERSION, 2);
18
+ frame.writeUInt8(packet.type, 3);
19
+ frame.writeUInt8(useCompression ? COMPRESSED : 0, 4);
20
+ frame.fill(0, 5, 8);
21
+ frame.writeBigUInt64BE(BigInt(packet.sequence), 8);
22
+ frame.writeUInt32BE(payload.length, 16);
23
+ payload.copy(frame, WIRE_HEADER_BYTES);
24
+ return { compressed: useCompression, data: frame, payloadBytes: source.length };
25
+ }
@@ -0,0 +1,13 @@
1
+ export { applyGridPatch } from "./applyGridPatch.js";
2
+ export { diffGridState } from "./diffGridState.js";
3
+ export { encodeWirePacket } from "./encodeWirePacket.js";
4
+ export { createGhosttyRemoteTerminalServer, GhosttyRemoteTerminalReplica, GhosttyRemoteTerminalServerDriver, ghosttySnapshotToGrid, } from "./GhosttyRemoteTerminal.js";
5
+ export { RemoteTerminalProtocolClient } from "./RemoteTerminalProtocolClient.js";
6
+ export type { RemoteTerminalReconnectState } from "./RemoteTerminalProtocolClient.js";
7
+ export { RemoteTerminalProtocolServer } from "./RemoteTerminalProtocolServer.js";
8
+ export { WirePacketDecoder } from "./WirePacketDecoder.js";
9
+ export { WirePacketType, type WirePacket } from "./WirePacket.js";
10
+ export { ThrottledTcpProxy, type ThrottledTcpProxyOptions } from "./testing/ThrottledTcpProxy.js";
11
+ export type { RemoteTerminalClientOptions, RemoteTerminalGridCell, RemoteTerminalGridPatch, RemoteTerminalGridRow, RemoteTerminalGridState, RemoteTerminalMode, RemoteTerminalProtocolMetrics, RemoteTerminalReplica, RemoteTerminalScrollbackPage, RemoteTerminalServerOptions, } from "./types.js";
12
+ export type { GhosttySnapshot, GhosttySnapshotCell, GhosttyTerminalLike, } from "./GhosttyRemoteTerminal.js";
13
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../sources/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AACnD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EACH,iCAAiC,EACjC,4BAA4B,EAC5B,iCAAiC,EACjC,qBAAqB,GACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,YAAY,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACtF,OAAO,EAAE,4BAA4B,EAAE,MAAM,mCAAmC,CAAC;AACjF,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,KAAK,wBAAwB,EAAE,MAAM,gCAAgC,CAAC;AAClG,YAAY,EACR,2BAA2B,EAC3B,sBAAsB,EACtB,uBAAuB,EACvB,qBAAqB,EACrB,uBAAuB,EACvB,kBAAkB,EAClB,6BAA6B,EAC7B,qBAAqB,EACrB,4BAA4B,EAC5B,2BAA2B,GAC9B,MAAM,YAAY,CAAC;AACpB,YAAY,EACR,eAAe,EACf,mBAAmB,EACnB,mBAAmB,GACtB,MAAM,4BAA4B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export { applyGridPatch } from "./applyGridPatch.js";
2
+ export { diffGridState } from "./diffGridState.js";
3
+ export { encodeWirePacket } from "./encodeWirePacket.js";
4
+ export { createGhosttyRemoteTerminalServer, GhosttyRemoteTerminalReplica, GhosttyRemoteTerminalServerDriver, ghosttySnapshotToGrid, } from "./GhosttyRemoteTerminal.js";
5
+ export { RemoteTerminalProtocolClient } from "./RemoteTerminalProtocolClient.js";
6
+ export { RemoteTerminalProtocolServer } from "./RemoteTerminalProtocolServer.js";
7
+ export { WirePacketDecoder } from "./WirePacketDecoder.js";
8
+ export { WirePacketType } from "./WirePacket.js";
9
+ export { ThrottledTcpProxy } from "./testing/ThrottledTcpProxy.js";
@@ -0,0 +1,3 @@
1
+ export declare function encodeJsonPayload(value: unknown): Uint8Array;
2
+ export declare function decodeJsonPayload<T>(value: Uint8Array): T;
3
+ //# sourceMappingURL=jsonPayload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsonPayload.d.ts","sourceRoot":"","sources":["../sources/jsonPayload.ts"],"names":[],"mappings":"AAGA,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAE5D;AAED,wBAAgB,iBAAiB,CAAC,CAAC,EAAE,KAAK,EAAE,UAAU,GAAG,CAAC,CAEzD"}
@@ -0,0 +1,8 @@
1
+ const encoder = new TextEncoder();
2
+ const decoder = new TextDecoder();
3
+ export function encodeJsonPayload(value) {
4
+ return encoder.encode(JSON.stringify(value));
5
+ }
6
+ export function decodeJsonPayload(value) {
7
+ return JSON.parse(decoder.decode(value));
8
+ }
@@ -0,0 +1,15 @@
1
+ export interface ThrottledTcpProxyOptions {
2
+ bytesPerSecond: number;
3
+ jitterMs?: number;
4
+ maxChunkBytes: number;
5
+ oneWayLatencyMs: number;
6
+ targetHost?: string;
7
+ targetPort: number;
8
+ }
9
+ export declare class ThrottledTcpProxy {
10
+ #private;
11
+ constructor(options: ThrottledTcpProxyOptions);
12
+ close(): Promise<void>;
13
+ listen(): Promise<number>;
14
+ }
15
+ //# sourceMappingURL=ThrottledTcpProxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThrottledTcpProxy.d.ts","sourceRoot":"","sources":["../../sources/testing/ThrottledTcpProxy.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,wBAAwB;IACrC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,iBAAiB;;gBAKd,OAAO,EAAE,wBAAwB;IAgBvC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAOtB,MAAM,IAAI,OAAO,CAAC,MAAM,CAAC;CA2BlC"}
@@ -0,0 +1,83 @@
1
+ import { createServer, Socket } from "node:net";
2
+ export class ThrottledTcpProxy {
3
+ #options;
4
+ #server;
5
+ #sockets = new Set();
6
+ constructor(options) {
7
+ if (!Number.isFinite(options.bytesPerSecond) ||
8
+ options.bytesPerSecond <= 0 ||
9
+ !Number.isSafeInteger(options.maxChunkBytes) ||
10
+ options.maxChunkBytes < 1 ||
11
+ !Number.isFinite(options.oneWayLatencyMs) ||
12
+ options.oneWayLatencyMs < 0 ||
13
+ !Number.isFinite(options.jitterMs ?? 0) ||
14
+ (options.jitterMs ?? 0) < 0) {
15
+ throw new Error("Invalid throttled TCP proxy options.");
16
+ }
17
+ this.#options = options;
18
+ }
19
+ async close() {
20
+ for (const socket of this.#sockets)
21
+ socket.destroy();
22
+ const server = this.#server;
23
+ if (server === undefined)
24
+ return;
25
+ await new Promise((resolve) => server.close(() => resolve()));
26
+ }
27
+ async listen() {
28
+ if (this.#server !== undefined)
29
+ throw new Error("The proxy is already listening.");
30
+ const server = createServer((client) => {
31
+ const upstream = new Socket();
32
+ this.#sockets.add(client).add(upstream);
33
+ client.once("close", () => this.#sockets.delete(client));
34
+ upstream.once("close", () => this.#sockets.delete(upstream));
35
+ upstream.connect(this.#options.targetPort, this.#options.targetHost ?? "127.0.0.1", () => {
36
+ forward(client, upstream, this.#options);
37
+ forward(upstream, client, this.#options);
38
+ });
39
+ client.once("error", () => upstream.destroy());
40
+ upstream.once("error", () => client.destroy());
41
+ });
42
+ this.#server = server;
43
+ await new Promise((resolve, reject) => {
44
+ server.once("error", reject);
45
+ server.listen(0, "127.0.0.1", resolve);
46
+ });
47
+ const address = server.address();
48
+ if (address === null || typeof address === "string")
49
+ throw new Error("Missing proxy port.");
50
+ return address.port;
51
+ }
52
+ }
53
+ function forward(source, destination, options) {
54
+ const jitterMs = options.jitterMs ?? 0;
55
+ let packet = 0;
56
+ let nextAvailableAt = 0;
57
+ let pendingChunks = 0;
58
+ let sourceEnded = false;
59
+ const finish = () => {
60
+ if (sourceEnded && pendingChunks === 0 && !destination.destroyed)
61
+ destination.end();
62
+ };
63
+ source.on("data", (data) => {
64
+ for (let offset = 0; offset < data.length; offset += options.maxChunkBytes) {
65
+ const chunk = data.subarray(offset, offset + options.maxChunkBytes);
66
+ const now = performance.now();
67
+ const jitter = jitterMs === 0 ? 0 : (packet++ % 3) - 1;
68
+ const readyAt = Math.max(now + options.oneWayLatencyMs + jitter * jitterMs, nextAvailableAt);
69
+ nextAvailableAt = readyAt + (chunk.length / options.bytesPerSecond) * 1_000;
70
+ pendingChunks += 1;
71
+ setTimeout(() => {
72
+ if (!destination.destroyed)
73
+ destination.write(chunk);
74
+ pendingChunks -= 1;
75
+ finish();
76
+ }, Math.max(0, readyAt - now)).unref();
77
+ }
78
+ });
79
+ source.once("end", () => {
80
+ sourceEnded = true;
81
+ finish();
82
+ });
83
+ }
@@ -0,0 +1,105 @@
1
+ import type { Duplex } from "node:stream";
2
+ export type RemoteTerminalMode = "grid" | "vt";
3
+ export interface RemoteTerminalGridCell {
4
+ styleId: number;
5
+ text: string;
6
+ width: 1 | 2;
7
+ x: number;
8
+ }
9
+ export interface RemoteTerminalGridRow {
10
+ cells: readonly RemoteTerminalGridCell[];
11
+ wrapped: boolean;
12
+ }
13
+ export interface RemoteTerminalGridState {
14
+ cols: number;
15
+ coversOutputOffset: number;
16
+ cursor: {
17
+ visible: boolean;
18
+ x: number;
19
+ y: number;
20
+ } | null;
21
+ palette: readonly string[];
22
+ revision: number;
23
+ rows: readonly RemoteTerminalGridRow[];
24
+ startRow: number;
25
+ styles: readonly Readonly<Record<string, unknown>>[];
26
+ title: string;
27
+ totalRows: number;
28
+ }
29
+ export interface RemoteTerminalGridPatch {
30
+ baseRevision: number;
31
+ cols: number;
32
+ coversOutputOffset: number;
33
+ cursor: RemoteTerminalGridState["cursor"];
34
+ palette: readonly string[];
35
+ revision: number;
36
+ rows: readonly (readonly [number, RemoteTerminalGridRow])[];
37
+ startRow: number;
38
+ styles: RemoteTerminalGridState["styles"];
39
+ title: string;
40
+ totalRows: number;
41
+ }
42
+ export interface RemoteTerminalReplica {
43
+ applyGrid(state: RemoteTerminalGridState): void | Promise<void>;
44
+ applyVt(data: Uint8Array): void | Promise<void>;
45
+ resize(cols: number, rows: number): void | Promise<void>;
46
+ }
47
+ export interface RemoteTerminalScrollbackPage {
48
+ baseRow: number;
49
+ count: number;
50
+ historyEpoch: string;
51
+ historyRevision: number;
52
+ palette?: readonly string[];
53
+ rows: readonly RemoteTerminalGridRow[];
54
+ start: number;
55
+ styles?: readonly Readonly<Record<string, unknown>>[];
56
+ totalRows: number;
57
+ }
58
+ export interface RemoteTerminalServerOptions {
59
+ epoch?: string;
60
+ initialCols?: number;
61
+ initialRows?: number;
62
+ maxBufferedBytes?: number;
63
+ maxInputLeases?: number;
64
+ maxFrameBytes?: number;
65
+ maxReplayBytes?: number;
66
+ maxUnacknowledgedBytes?: number;
67
+ onBeforeResize?: () => void | Promise<void>;
68
+ onFlowControl?: (paused: boolean) => void;
69
+ onInput: (data: Uint8Array) => void | Promise<void>;
70
+ onResize: (cols: number, rows: number) => void | Promise<void>;
71
+ onScrollback?: (start: number, count: number, basis?: {
72
+ historyEpoch: string;
73
+ historyRevision: number;
74
+ }) => RemoteTerminalScrollbackPage | Promise<RemoteTerminalScrollbackPage>;
75
+ parserFingerprint?: string;
76
+ wireChunkBytes?: number;
77
+ }
78
+ export interface RemoteTerminalClientOptions {
79
+ capabilities?: {
80
+ grid: boolean;
81
+ vt: boolean;
82
+ };
83
+ clientId: string;
84
+ creditBytes?: number;
85
+ epoch?: string;
86
+ inputLease?: string;
87
+ pendingInputs?: readonly {
88
+ data: Uint8Array;
89
+ sequence: number;
90
+ }[];
91
+ onExit?: (exitCode: number | null) => void;
92
+ onMode?: (mode: RemoteTerminalMode) => void;
93
+ parserFingerprint?: string;
94
+ replica: RemoteTerminalReplica;
95
+ resumeInputSequence?: number;
96
+ resumeOutputOffset?: number;
97
+ stream: Duplex;
98
+ }
99
+ export interface RemoteTerminalProtocolMetrics {
100
+ compressedPackets: number;
101
+ encodedPackets: number;
102
+ payloadBytes: number;
103
+ wireBytes: number;
104
+ }
105
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../sources/types.ts"],"names":[],"mappings":"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"}
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@slopus/ghostty-web",
3
+ "version": "0.1.0",
4
+ "description": "Client/server protocol for efficiently remoting a Ghostty-backed terminal to web and native clients.",
5
+ "license": "MIT",
6
+ "files": [
7
+ "dist",
8
+ "LICENSE",
9
+ "README.md"
10
+ ],
11
+ "type": "module",
12
+ "exports": {
13
+ ".": {
14
+ "types": "./dist/index.d.ts",
15
+ "default": "./dist/index.js"
16
+ }
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^24.0.10",
23
+ "typescript": "^5.8.3",
24
+ "vitest": "^3.2.4",
25
+ "@slopus/rig-gym": "0.0.0"
26
+ },
27
+ "scripts": {
28
+ "build": "tsc -p tsconfig.build.json",
29
+ "check": "tsc --noEmit",
30
+ "test": "vitest run"
31
+ }
32
+ }