socketduct 0.1.25 → 0.1.27
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/AGENTS.md +2 -2
- package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
- package/changelog.d/20261025070000-reclaim-unreadable-retention-lock.md +14 -0
- package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
- package/package.json +2 -2
- package/scripts/test.js +9 -14
- package/src/logging/compressed-log-writer.js +1 -1
- package/src/logging/log-retention.js +36 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- package/test/e2e/gateway-process.test.js +93 -92
- package/test/e2e/logger-shutdown-process.test.js +40 -37
- package/test/e2e/reverse-client-recovery-process.test.js +107 -103
- package/test/e2e/reverse-gateway-process.test.js +328 -318
- package/test/integration/initial-target-recovery.test.js +333 -334
- package/test/integration/logging-cli.test.js +121 -119
- package/test/integration/logging-compressed-recovery.test.js +96 -95
- package/test/integration/multiplexed-compatibility.test.js +178 -177
- package/test/integration/multiplexed-corrections.test.js +360 -359
- package/test/integration/multiplexed-failures.test.js +253 -251
- package/test/integration/multiplexed-transport-set.test.js +334 -322
- package/test/integration/multiplexed-transport.test.js +135 -134
- package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
- package/test/integration/reverse-gateway.test.js +2309 -2218
- package/test/integration/reverse-resume-continuity.test.js +219 -218
- package/test/integration/reverse-route-class.test.js +358 -341
- package/test/integration/socketduct-http-agent.test.js +314 -312
- package/test/integration/socketduct-https-agent.test.js +516 -485
- package/test/integration/stream-compression.test.js +862 -824
- package/test/integration/tcp-forwarding.test.js +2550 -2447
- package/test/integration/tls-forwarding.test.js +593 -592
- package/test/unit/auth/auth.test.js +43 -44
- package/test/unit/auth/static-token-store.test.js +9 -8
- package/test/unit/client/connect.test.js +41 -40
- package/test/unit/client/deferred-virtual-socket.test.js +95 -94
- package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
- package/test/unit/client/multiplexed-transport-set.test.js +830 -748
- package/test/unit/client/socketduct-socket.test.js +768 -771
- package/test/unit/client/virtual-socket.test.js +198 -168
- package/test/unit/config/docker-gateway-files.test.js +224 -224
- package/test/unit/config/environment.test.js +472 -492
- package/test/unit/index.test.js +53 -52
- package/test/unit/logging/compressed-log-worker.test.js +23 -22
- package/test/unit/logging/compressed-log-writer.test.js +155 -154
- package/test/unit/logging/log-retention.test.js +71 -48
- package/test/unit/logging/log-segment-index.test.js +84 -81
- package/test/unit/logging/zstd-frame-codec.test.js +28 -19
- package/test/unit/protocol/compression.test.js +254 -298
- package/test/unit/protocol/frame.test.js +280 -276
- package/test/unit/protocol/v3-compression.test.js +21 -23
- package/test/unit/protocol/v3-frame.test.js +42 -41
- package/test/unit/release-configuration.test.js +15 -12
- package/test/unit/reverse/disconnect-attribution.test.js +65 -64
- package/test/unit/reverse-terminal-outcomes.test.js +53 -50
- package/test/unit/server/control-handler.test.js +232 -231
- package/test/unit/server/primary.test.js +51 -49
- package/test/unit/server/relay-runtime.test.js +280 -294
- package/test/unit/sessions/session.test.js +48 -62
- package/test/unit/spool/spool-file.test.js +401 -385
|
@@ -1,336 +1,340 @@
|
|
|
1
|
-
import
|
|
2
|
-
import test from "node:test"
|
|
1
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
3
2
|
|
|
4
3
|
import {FrameDecoder, encodeCompactAck, encodeCompactData, encodeFrame} from "../../../src/protocol/frame.js"
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
describe("frame protocol", () => {
|
|
6
|
+
it("encodeFrame and FrameDecoder preserve header fields and payload bytes", () => {
|
|
7
|
+
const payload = Buffer.from([0, 1, 2, 253, 254, 255])
|
|
8
|
+
const encoded = encodeFrame({type: "stream.data", sessionId: "session-1", sequence: 42}, payload)
|
|
9
|
+
|
|
10
|
+
const decoder = new FrameDecoder()
|
|
11
|
+
const frames = decoder.push(encoded)
|
|
12
|
+
|
|
13
|
+
expect(frames.length).toBe(1)
|
|
14
|
+
expect(frames[0]?.header).toEqual({
|
|
15
|
+
type: "stream.data",
|
|
16
|
+
sessionId: "session-1",
|
|
17
|
+
sequence: 42
|
|
18
|
+
})
|
|
19
|
+
if (!frames[0]) throw new Error("expected a decoded stream.data frame")
|
|
20
|
+
expect(Buffer.from(frames[0].payload).equals(Buffer.from(payload))).toBe(true)
|
|
21
|
+
expect(frames[0]?.wireFormat).toBe("envelope")
|
|
22
|
+
})
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
type: "
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
it("FrameDecoder decodes frames split across multiple TCP chunks", () => {
|
|
25
|
+
const first = encodeFrame({type: "auth.request", requestId: "a"}, Buffer.from("one"))
|
|
26
|
+
const second = encodeFrame({type: "session.create", requestId: "b"}, Buffer.from("two"))
|
|
27
|
+
const combined = Buffer.concat([first, second])
|
|
28
|
+
const decoder = new FrameDecoder()
|
|
29
|
+
|
|
30
|
+
expect(decoder.push(combined.subarray(0, 2))).toEqual([])
|
|
31
|
+
expect(decoder.push(combined.subarray(2, 9))).toEqual([])
|
|
32
|
+
const frames = decoder.push(combined.subarray(9))
|
|
33
|
+
|
|
34
|
+
expect(frames.length).toBe(2)
|
|
35
|
+
expect(frames[0]?.header.type).toBe("auth.request")
|
|
36
|
+
expect(frames[0]?.payload.toString("utf8")).toBe("one")
|
|
37
|
+
expect(frames[1]?.header.type).toBe("session.create")
|
|
38
|
+
expect(frames[1]?.payload.toString("utf8")).toBe("two")
|
|
18
39
|
})
|
|
19
|
-
assert.deepEqual(frames[0]?.payload, payload)
|
|
20
|
-
assert.equal(frames[0]?.wireFormat, "envelope")
|
|
21
|
-
})
|
|
22
40
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const decoder = new FrameDecoder()
|
|
28
|
-
|
|
29
|
-
assert.deepEqual(decoder.push(combined.subarray(0, 2)), [])
|
|
30
|
-
assert.deepEqual(decoder.push(combined.subarray(2, 9)), [])
|
|
31
|
-
const frames = decoder.push(combined.subarray(9))
|
|
32
|
-
|
|
33
|
-
assert.equal(frames.length, 2)
|
|
34
|
-
assert.equal(frames[0]?.header.type, "auth.request")
|
|
35
|
-
assert.equal(frames[0]?.payload.toString("utf8"), "one")
|
|
36
|
-
assert.equal(frames[1]?.header.type, "session.create")
|
|
37
|
-
assert.equal(frames[1]?.payload.toString("utf8"), "two")
|
|
38
|
-
})
|
|
41
|
+
it("FrameDecoder rejects invalid header lengths before allocating payload buffers", () => {
|
|
42
|
+
const decoder = new FrameDecoder({maxHeaderBytes: 8})
|
|
43
|
+
const invalid = Buffer.alloc(4)
|
|
44
|
+
invalid.writeUInt32BE(9, 0)
|
|
39
45
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const invalid = Buffer.alloc(4)
|
|
43
|
-
invalid.writeUInt32BE(9, 0)
|
|
46
|
+
expect(() => decoder.push(invalid)).toThrow(/header length/i)
|
|
47
|
+
})
|
|
44
48
|
|
|
45
|
-
|
|
46
|
-
})
|
|
49
|
+
it("FrameDecoder rejects payloads larger than the configured limit", () => {
|
|
50
|
+
const decoder = new FrameDecoder({maxPayloadBytes: 2})
|
|
51
|
+
const encoded = encodeFrame({type: "stream.data"}, Buffer.from("too-large"))
|
|
47
52
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const encoded = encodeFrame({type: "stream.data"}, Buffer.from("too-large"))
|
|
53
|
+
expect(() => decoder.push(encoded)).toThrow(/payload length/i)
|
|
54
|
+
})
|
|
51
55
|
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
it("FrameDecoder handles empty chunk without error", () => {
|
|
57
|
+
const decoder = new FrameDecoder()
|
|
58
|
+
const frames = decoder.push(Buffer.alloc(0))
|
|
59
|
+
expect(frames).toEqual([])
|
|
60
|
+
})
|
|
54
61
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
})
|
|
62
|
+
it("compact v2 uncompressed data and ACK records round-trip beside envelope controls", () => {
|
|
63
|
+
const decoder = new FrameDecoder({compactDirection: "target-to-client"})
|
|
64
|
+
const frames = decoder.push(Buffer.concat([
|
|
65
|
+
encodeFrame({type: "session.resumed", identifier: "s1", framing: 2}),
|
|
66
|
+
encodeCompactData({sequence: 300, payload: Buffer.from("raw")}),
|
|
67
|
+
encodeCompactAck(257),
|
|
68
|
+
encodeFrame({type: "session.close", direction: "target-to-client"})
|
|
69
|
+
]))
|
|
70
|
+
expect(frames.map(({header, payload}) => ({header, payload: payload.toString()}))).toEqual([
|
|
71
|
+
{header: {type: "session.resumed", identifier: "s1", framing: 2}, payload: ""},
|
|
72
|
+
{header: {type: "stream.data", direction: "target-to-client", sequence: 300, compact: true}, payload: "raw"},
|
|
73
|
+
{header: {type: "stream.ack", direction: "client-to-target", ackSequence: 257, compact: true}, payload: ""},
|
|
74
|
+
{header: {type: "session.close", direction: "target-to-client"}, payload: ""}
|
|
75
|
+
])
|
|
76
|
+
})
|
|
60
77
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
assert.deepEqual(frames.map(({header, payload}) => ({header, payload: payload.toString()})), [
|
|
70
|
-
{header: {type: "session.resumed", identifier: "s1", framing: 2}, payload: ""},
|
|
71
|
-
{header: {type: "stream.data", direction: "target-to-client", sequence: 300, compact: true}, payload: "raw"},
|
|
72
|
-
{header: {type: "stream.ack", direction: "client-to-target", ackSequence: 257, compact: true}, payload: ""},
|
|
73
|
-
{header: {type: "session.close", direction: "target-to-client"}, payload: ""}
|
|
74
|
-
])
|
|
75
|
-
})
|
|
78
|
+
it("compact v2 zstd records normalize compression metadata", () => {
|
|
79
|
+
const decoder = new FrameDecoder({compactDirection: "client-to-target"})
|
|
80
|
+
const [frame] = decoder.push(encodeCompactData({sequence: Number.MAX_SAFE_INTEGER, dataLength: 4096, payload: Buffer.from([1, 2, 3])}))
|
|
81
|
+
expect(frame?.header).toEqual({type: "stream.data", direction: "client-to-target", sequence: Number.MAX_SAFE_INTEGER, compact: true, encoding: "zstd", dataLength: 4096})
|
|
82
|
+
if (!frame) throw new Error("expected a decoded compact frame")
|
|
83
|
+
expect(Buffer.from(frame.payload).equals(Buffer.from([1, 2, 3]))).toBe(true)
|
|
84
|
+
expect(frame?.wireFormat).toBe("compact")
|
|
85
|
+
})
|
|
76
86
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
})
|
|
87
|
+
it("compact decoder waits for fragmented records", () => {
|
|
88
|
+
const encoded = encodeCompactData({sequence: 129, payload: Buffer.from("fragmented")})
|
|
89
|
+
const decoder = new FrameDecoder({compactDirection: "client-to-target"})
|
|
90
|
+
expect(decoder.push(encoded.subarray(0, 3))).toEqual([])
|
|
91
|
+
expect(decoder.push(encoded.subarray(3))[0]?.payload.toString()).toBe("fragmented")
|
|
92
|
+
})
|
|
84
93
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
})
|
|
94
|
+
it("compact decoder rejects non-canonical, overflowing, and oversized varints", () => {
|
|
95
|
+
const options = /** @type {const} */ ({compactDirection: "client-to-target"})
|
|
96
|
+
expect(() => new FrameDecoder(options).push(Buffer.from([0x82, 0x80, 0x00]))).toThrow(/canonical/)
|
|
97
|
+
expect(() => new FrameDecoder(options).push(Buffer.from([0x82, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x7f]))).toThrow(/safe integer/)
|
|
98
|
+
expect(() => new FrameDecoder({...options, maxPayloadBytes: 2}).push(encodeCompactData({sequence: 3, payload: Buffer.from("abc")}))).toThrow(/payload length/)
|
|
99
|
+
})
|
|
91
100
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
assert.throws(() => new FrameDecoder({...options, maxPayloadBytes: 2}).push(encodeCompactData({sequence: 3, payload: Buffer.from("abc")})), /payload length/)
|
|
97
|
-
})
|
|
101
|
+
it("FrameDecoder handles partial uint32 header length", () => {
|
|
102
|
+
const decoder = new FrameDecoder()
|
|
103
|
+
const partial = Buffer.alloc(2)
|
|
104
|
+
partial.writeUInt16BE(42, 0)
|
|
98
105
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
const partial = Buffer.alloc(2)
|
|
102
|
-
partial.writeUInt16BE(42, 0)
|
|
106
|
+
expect(decoder.push(partial)).toEqual([])
|
|
107
|
+
})
|
|
103
108
|
|
|
104
|
-
|
|
105
|
-
|
|
109
|
+
it("FrameDecoder handles partial header bytes after header length", () => {
|
|
110
|
+
const decoder = new FrameDecoder()
|
|
111
|
+
const buf = Buffer.alloc(6)
|
|
112
|
+
buf.writeUInt32BE(10, 0)
|
|
106
113
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const buf = Buffer.alloc(6)
|
|
110
|
-
buf.writeUInt32BE(10, 0)
|
|
114
|
+
expect(decoder.push(buf)).toEqual([])
|
|
115
|
+
})
|
|
111
116
|
|
|
112
|
-
|
|
113
|
-
})
|
|
117
|
+
it("FrameDecoder handles partial payload length uint32 after header", () => {
|
|
118
|
+
const header = JSON.stringify({type: "test"})
|
|
119
|
+
const buf = Buffer.alloc(4 + header.length + 2)
|
|
120
|
+
buf.writeUInt32BE(header.length, 0)
|
|
121
|
+
buf.write(header, 4, "utf8")
|
|
114
122
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
buf.writeUInt32BE(header.length, 0)
|
|
119
|
-
buf.write(header, 4, "utf8")
|
|
123
|
+
const decoder = new FrameDecoder()
|
|
124
|
+
expect(decoder.push(buf)).toEqual([])
|
|
125
|
+
})
|
|
120
126
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
127
|
+
it("FrameDecoder handles partial payload bytes after full header and payload length", () => {
|
|
128
|
+
const header = JSON.stringify({type: "test"})
|
|
129
|
+
const buf = Buffer.alloc(4 + header.length + 4 + 3)
|
|
130
|
+
buf.writeUInt32BE(header.length, 0)
|
|
131
|
+
buf.write(header, 4, "utf8")
|
|
132
|
+
buf.writeUInt32BE(10, 4 + header.length)
|
|
124
133
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
buf.writeUInt32BE(header.length, 0)
|
|
129
|
-
buf.write(header, 4, "utf8")
|
|
130
|
-
buf.writeUInt32BE(10, 4 + header.length)
|
|
134
|
+
const decoder = new FrameDecoder()
|
|
135
|
+
expect(decoder.push(buf)).toEqual([])
|
|
136
|
+
})
|
|
131
137
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
})
|
|
138
|
+
it("FrameDecoder decodes one frame from a chunk that arrives byte-by-byte (uint32 split)", () => {
|
|
139
|
+
const payload = Buffer.from("hello")
|
|
140
|
+
const encoded = encodeFrame({type: "ping"}, payload)
|
|
141
|
+
const decoder = new FrameDecoder()
|
|
135
142
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
assert.equal(result.length, 1)
|
|
147
|
-
assert.equal(result[0]?.header.type, "ping")
|
|
148
|
-
assert.deepEqual(result[0]?.payload, payload)
|
|
143
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
144
|
+
const result = decoder.push(encoded.subarray(i, i + 1))
|
|
145
|
+
if (i < encoded.length - 1) {
|
|
146
|
+
expect(result).toEqual([])
|
|
147
|
+
} else {
|
|
148
|
+
expect(result.length).toBe(1)
|
|
149
|
+
expect(result[0]?.header.type).toBe("ping")
|
|
150
|
+
if (!result[0]) throw new Error("expected a decoded ping frame")
|
|
151
|
+
expect(Buffer.from(result[0].payload).equals(Buffer.from(payload))).toBe(true)
|
|
152
|
+
}
|
|
149
153
|
}
|
|
150
|
-
}
|
|
151
|
-
})
|
|
154
|
+
})
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
})
|
|
156
|
+
it("FrameDecoder decodes multiple complete frames from a single push", () => {
|
|
157
|
+
const frame1 = encodeFrame({type: "alpha"}, Buffer.from("a"))
|
|
158
|
+
const frame2 = encodeFrame({type: "beta"}, Buffer.from("bb"))
|
|
159
|
+
const frame3 = encodeFrame({type: "gamma"}, Buffer.from("ccc"))
|
|
160
|
+
const combined = Buffer.concat([frame1, frame2, frame3])
|
|
161
|
+
|
|
162
|
+
const decoder = new FrameDecoder()
|
|
163
|
+
const frames = decoder.push(combined)
|
|
164
|
+
|
|
165
|
+
expect(frames.length).toBe(3)
|
|
166
|
+
expect(frames[0]?.header.type).toBe("alpha")
|
|
167
|
+
expect(frames[0]?.payload.toString()).toBe("a")
|
|
168
|
+
expect(frames[1]?.header.type).toBe("beta")
|
|
169
|
+
expect(frames[1]?.payload.toString()).toBe("bb")
|
|
170
|
+
expect(frames[2]?.header.type).toBe("gamma")
|
|
171
|
+
expect(frames[2]?.payload.toString()).toBe("ccc")
|
|
172
|
+
})
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
it("FrameDecoder with maxInputBytes rejects over-budget input", () => {
|
|
175
|
+
const decoder = new FrameDecoder({maxInputBytes: 10})
|
|
176
|
+
const frame = encodeFrame({type: "test"}, Buffer.from("hello"))
|
|
174
177
|
|
|
175
|
-
|
|
176
|
-
})
|
|
178
|
+
expect(() => decoder.push(frame)).toThrow(/input byte budget/i)
|
|
179
|
+
})
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
it("FrameDecoder with maxInputBytes allows input within budget and rejects on overflow", () => {
|
|
182
|
+
const decoder = new FrameDecoder({maxInputBytes: 100})
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
184
|
+
const header = JSON.stringify({type: "small"})
|
|
185
|
+
const smallFrame = Buffer.alloc(4 + header.length + 4 + 5)
|
|
186
|
+
smallFrame.writeUInt32BE(header.length, 0)
|
|
187
|
+
smallFrame.write(header, 4, "utf8")
|
|
188
|
+
smallFrame.writeUInt32BE(5, 4 + header.length)
|
|
189
|
+
smallFrame.write("hello", 4 + header.length + 4, "utf8")
|
|
187
190
|
|
|
188
|
-
|
|
189
|
-
|
|
191
|
+
const frames = decoder.push(smallFrame.subarray(0, 7))
|
|
192
|
+
expect(frames).toEqual([])
|
|
190
193
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
const frames2 = decoder.push(smallFrame.subarray(7))
|
|
195
|
+
expect(frames2.length).toBe(1)
|
|
196
|
+
expect(frames2[0]?.header.type).toBe("small")
|
|
194
197
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
})
|
|
198
|
+
const big = Buffer.alloc(2000)
|
|
199
|
+
big.writeUInt32BE(5, 0)
|
|
200
|
+
expect(() => decoder.push(big)).toThrow(/input byte budget/i)
|
|
201
|
+
})
|
|
199
202
|
|
|
200
|
-
|
|
201
|
-
|
|
203
|
+
it("FrameDecoder with zero maxInputBytes disables budget check", () => {
|
|
204
|
+
const decoder = new FrameDecoder({maxInputBytes: 0, maxHeaderBytes: 200_000})
|
|
202
205
|
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
const smallChunk = Buffer.alloc(100_001)
|
|
207
|
+
smallChunk.writeUInt32BE(100_000, 0)
|
|
205
208
|
|
|
206
|
-
|
|
207
|
-
})
|
|
209
|
+
expect(decoder.push(smallChunk)).toEqual([])
|
|
210
|
+
})
|
|
208
211
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
212
|
+
it("FrameDecoder byte-by-byte input exercises cursor path: one frame", () => {
|
|
213
|
+
const payload = Buffer.from("cursor")
|
|
214
|
+
const encoded = encodeFrame({type: "cursor-test"}, payload)
|
|
215
|
+
const decoder = new FrameDecoder()
|
|
213
216
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
217
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
218
|
+
const result = decoder.push(encoded.subarray(i, i + 1))
|
|
219
|
+
if (i < encoded.length - 1) {
|
|
220
|
+
expect(result).toEqual([])
|
|
221
|
+
}
|
|
218
222
|
}
|
|
219
|
-
}
|
|
220
223
|
|
|
221
|
-
|
|
224
|
+
const frames = decoder.push(Buffer.alloc(0))
|
|
222
225
|
|
|
223
|
-
|
|
224
|
-
})
|
|
226
|
+
expect(frames).toEqual([])
|
|
227
|
+
})
|
|
225
228
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
229
|
+
it("FrameDecoder byte-by-byte input exercises cursor path: multiple queued frames", () => {
|
|
230
|
+
const frame1 = encodeFrame({type: "alpha"}, Buffer.from("a"))
|
|
231
|
+
const frame2 = encodeFrame({type: "beta"}, Buffer.from("bb"))
|
|
232
|
+
const frame3 = encodeFrame({type: "gamma"}, Buffer.from("ccc"))
|
|
233
|
+
const combined = Buffer.concat([frame1, frame2, frame3])
|
|
234
|
+
const decoder = new FrameDecoder()
|
|
235
|
+
|
|
236
|
+
/** @type {import('../../../src/protocol/frame.js').Frame[]} */
|
|
237
|
+
const allFrames = []
|
|
238
|
+
for (let i = 0; i < combined.length; i++) {
|
|
239
|
+
const result = decoder.push(combined.subarray(i, i + 1))
|
|
240
|
+
for (const f of result) {
|
|
241
|
+
allFrames.push(f)
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
for (const f of decoder.push(Buffer.alloc(0))) {
|
|
238
246
|
allFrames.push(f)
|
|
239
247
|
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
for (const f of decoder.push(Buffer.alloc(0))) {
|
|
243
|
-
allFrames.push(f)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
assert.equal(allFrames.length, 3)
|
|
247
|
-
assert.equal(allFrames[0]?.header.type, "alpha")
|
|
248
|
-
assert.equal(allFrames[0]?.payload.toString(), "a")
|
|
249
|
-
assert.equal(allFrames[1]?.header.type, "beta")
|
|
250
|
-
assert.equal(allFrames[1]?.payload.toString(), "bb")
|
|
251
|
-
assert.equal(allFrames[2]?.header.type, "gamma")
|
|
252
|
-
assert.equal(allFrames[2]?.payload.toString(), "ccc")
|
|
253
|
-
})
|
|
254
248
|
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
expect(allFrames.length).toBe(3)
|
|
250
|
+
expect(allFrames[0]?.header.type).toBe("alpha")
|
|
251
|
+
expect(allFrames[0]?.payload.toString()).toBe("a")
|
|
252
|
+
expect(allFrames[1]?.header.type).toBe("beta")
|
|
253
|
+
expect(allFrames[1]?.payload.toString()).toBe("bb")
|
|
254
|
+
expect(allFrames[2]?.header.type).toBe("gamma")
|
|
255
|
+
expect(allFrames[2]?.payload.toString()).toBe("ccc")
|
|
256
|
+
})
|
|
257
257
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
})
|
|
258
|
+
it("FrameDecoder discards header read from garbage bytes and throws on oversized header", () => {
|
|
259
|
+
const decoder = new FrameDecoder()
|
|
261
260
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
const combined = Buffer.concat([frame1, frame2])
|
|
266
|
-
const decoder = new FrameDecoder()
|
|
267
|
-
|
|
268
|
-
/** @type {import('../../../src/protocol/frame.js').Frame[]} */
|
|
269
|
-
const allFrames = []
|
|
270
|
-
for (const f of decoder.push(combined.subarray(0, frame1.length + 2))) {
|
|
271
|
-
allFrames.push(f)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
for (const f of decoder.push(combined.subarray(frame1.length + 2))) {
|
|
275
|
-
allFrames.push(f)
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
for (const f of decoder.push(Buffer.alloc(0))) {
|
|
279
|
-
allFrames.push(f)
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
assert.equal(allFrames.length, 2)
|
|
283
|
-
assert.equal(allFrames[0]?.header.type, "first")
|
|
284
|
-
assert.equal(allFrames[1]?.header.type, "second")
|
|
285
|
-
})
|
|
261
|
+
const garbage = Buffer.alloc(5000, 0xFF)
|
|
262
|
+
expect(() => decoder.push(garbage)).toThrow(/header length/i)
|
|
263
|
+
})
|
|
286
264
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
265
|
+
it("FrameDecoder compaction path: readPos shifts after partial consumption triggers compact", () => {
|
|
266
|
+
const frame1 = encodeFrame({type: "first"}, Buffer.from("data1"))
|
|
267
|
+
const frame2 = encodeFrame({type: "second"}, Buffer.from("data2"))
|
|
268
|
+
const combined = Buffer.concat([frame1, frame2])
|
|
269
|
+
const decoder = new FrameDecoder()
|
|
291
270
|
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
for (
|
|
295
|
-
|
|
296
|
-
totalFrames += result.length
|
|
271
|
+
/** @type {import('../../../src/protocol/frame.js').Frame[]} */
|
|
272
|
+
const allFrames = []
|
|
273
|
+
for (const f of decoder.push(combined.subarray(0, frame1.length + 2))) {
|
|
274
|
+
allFrames.push(f)
|
|
297
275
|
}
|
|
298
|
-
}
|
|
299
276
|
|
|
300
|
-
|
|
277
|
+
for (const f of decoder.push(combined.subarray(frame1.length + 2))) {
|
|
278
|
+
allFrames.push(f)
|
|
279
|
+
}
|
|
301
280
|
|
|
302
|
-
|
|
303
|
-
|
|
281
|
+
for (const f of decoder.push(Buffer.alloc(0))) {
|
|
282
|
+
allFrames.push(f)
|
|
283
|
+
}
|
|
304
284
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
285
|
+
expect(allFrames.length).toBe(2)
|
|
286
|
+
expect(allFrames[0]?.header.type).toBe("first")
|
|
287
|
+
expect(allFrames[1]?.header.type).toBe("second")
|
|
288
|
+
})
|
|
309
289
|
|
|
310
|
-
decoder
|
|
290
|
+
it("FrameDecoder verifies the decoder is reusable across many byte-by-byte deliveries", () => {
|
|
291
|
+
const payload = Buffer.from("large-frame")
|
|
292
|
+
const encoded = encodeFrame({type: "reuse"}, payload)
|
|
293
|
+
const decoder = new FrameDecoder()
|
|
294
|
+
|
|
295
|
+
let totalFrames = 0
|
|
296
|
+
for (let round = 0; round < 5; round++) {
|
|
297
|
+
for (let i = 0; i < encoded.length; i++) {
|
|
298
|
+
const result = decoder.push(encoded.subarray(i, i + 1))
|
|
299
|
+
totalFrames += result.length
|
|
300
|
+
}
|
|
301
|
+
}
|
|
311
302
|
|
|
312
|
-
|
|
313
|
-
const after = decoder.push(largeFrame)
|
|
314
|
-
assert.equal(after.length, 1)
|
|
315
|
-
assert.equal(after[0]?.header.type, "large")
|
|
316
|
-
})
|
|
303
|
+
totalFrames += decoder.push(Buffer.alloc(0)).length
|
|
317
304
|
|
|
318
|
-
|
|
319
|
-
|
|
305
|
+
expect(totalFrames).toBe(5)
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
it("FrameDecoder disableInputBudget allows exceeding original budget", () => {
|
|
309
|
+
const decoder = new FrameDecoder({maxInputBytes: 100})
|
|
310
|
+
const smallFrame = encodeFrame({type: "small"}, Buffer.from("hello"))
|
|
311
|
+
decoder.push(smallFrame)
|
|
312
|
+
|
|
313
|
+
decoder.disableInputBudget()
|
|
314
|
+
|
|
315
|
+
const largeFrame = encodeFrame({type: "large"}, Buffer.allocUnsafe(200_000))
|
|
316
|
+
const after = decoder.push(largeFrame)
|
|
317
|
+
expect(after.length).toBe(1)
|
|
318
|
+
expect(after[0]?.header.type).toBe("large")
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it("FrameDecoder compaction reuses existing capacity instead of unbounded growth", () => {
|
|
322
|
+
const decoder = new FrameDecoder()
|
|
320
323
|
|
|
321
|
-
|
|
322
|
-
|
|
324
|
+
const manyBytes = Buffer.alloc(1000, 0x41)
|
|
325
|
+
const bigFrame = encodeFrame({type: "big"}, manyBytes)
|
|
323
326
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
327
|
+
const part1 = bigFrame.subarray(0, 800)
|
|
328
|
+
const part2 = bigFrame.subarray(800)
|
|
329
|
+
decoder.push(part1)
|
|
330
|
+
const extracted = decoder.push(part2)
|
|
331
|
+
expect(extracted.length).toBe(1)
|
|
329
332
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
333
|
+
for (let round = 0; round < 20; round++) {
|
|
334
|
+
const inner = encodeFrame({type: "small"}, Buffer.from("x"))
|
|
335
|
+
const result = decoder.push(inner)
|
|
336
|
+
expect(result.length).toBe(1)
|
|
337
|
+
expect(result[0]?.header.type).toBe("small")
|
|
338
|
+
}
|
|
339
|
+
})
|
|
336
340
|
})
|