1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
|
import { describe, expect, it } from "vitest";
import { encode, FrameDecoder } from "./framing.js";
describe("encode", () => {
it("produces correct Content-Length header", () => {
const msg = '{"jsonrpc":"2.0","id":1,"method":"initialize"}';
const encoded = encode(msg);
const text = new TextDecoder().decode(encoded);
expect(text).toBe(`Content-Length: ${new TextEncoder().encode(msg).length}\r\n\r\n${msg}`);
});
it("handles empty message", () => {
const encoded = encode("");
const text = new TextDecoder().decode(encoded);
expect(text).toBe("Content-Length: 0\r\n\r\n");
});
});
describe("FrameDecoder", () => {
it("reassembles a complete message from one chunk", () => {
const msg = '{"jsonrpc":"2.0","id":1}';
const encoded = encode(msg);
const decoder = new FrameDecoder();
const messages = decoder.decode(encoded);
expect(messages).toEqual([msg]);
});
it("handles split across chunks", () => {
const msg = '{"jsonrpc":"2.0","id":1,"method":"initialize"}';
const encoded = encode(msg);
const mid = Math.floor(encoded.length / 2);
const chunk1 = encoded.slice(0, mid);
const chunk2 = encoded.slice(mid);
const decoder = new FrameDecoder();
const result1 = decoder.decode(chunk1);
expect(result1).toEqual([]);
const result2 = decoder.decode(chunk2);
expect(result2).toEqual([msg]);
});
it("handles two messages in one chunk", () => {
const msg1 = '{"jsonrpc":"2.0","id":1}';
const msg2 = '{"jsonrpc":"2.0","id":2}';
const encoded1 = encode(msg1);
const encoded2 = encode(msg2);
const combined = new Uint8Array(encoded1.length + encoded2.length);
combined.set(encoded1);
combined.set(encoded2, encoded1.length);
const decoder = new FrameDecoder();
const messages = decoder.decode(combined);
expect(messages).toEqual([msg1, msg2]);
});
it("rejects negative Content-Length by skipping header", () => {
const header = "Content-Length: -5\r\n\r\n";
const encoded = new TextEncoder().encode(`${header}extra`);
const decoder = new FrameDecoder();
const messages = decoder.decode(encoded);
// Negative length does not match the digit capture, so the header is skipped.
expect(messages).toEqual([]);
});
it("rejects zero Content-Length", () => {
const encoded = encode("");
const decoder = new FrameDecoder();
const messages = decoder.decode(encoded);
expect(messages).toEqual([""]);
});
it("reassembles multi-byte UTF-8 content (byte-length, not char-length)", () => {
// "héllo" — é is two UTF-8 bytes; Content-Length counts bytes.
const msg = '{"text":"héllo 🚀"}';
const encoded = encode(msg);
expect(new TextEncoder().encode(msg).length).toBeGreaterThan(msg.length);
const decoder = new FrameDecoder();
const messages = decoder.decode(encoded);
expect(messages).toEqual([msg]);
});
it("reassembles multi-byte content split across a chunk boundary", () => {
const msg = '{"text":"日本語のテスト"}';
const encoded = encode(msg);
const mid = Math.floor(encoded.length / 2);
const decoder = new FrameDecoder();
expect(decoder.decode(encoded.slice(0, mid))).toEqual([]);
expect(decoder.decode(encoded.slice(mid))).toEqual([msg]);
});
});
|