socketduct 0.1.24 → 0.1.26
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/docs/runbooks/jsonl-integrity-rollout.md +8 -6
- package/package.json +2 -2
- package/scripts/test.js +9 -14
- package/src/client/multiplexed-transport-set.js +20 -2
- package/src/logging/compressed-log-writer.js +1 -1
- package/src/logging/zstd-frame-codec.js +15 -1
- package/src/reverse/client.js +44 -1
- package/src/reverse/hub.js +10 -1
- package/src/reverse/runtime.js +11 -2
- 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 +363 -278
- 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 +845 -732
- 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 +52 -47
- 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,6 +1,5 @@
|
|
|
1
1
|
/* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param-type, jsdoc/require-returns */
|
|
2
|
-
import
|
|
3
|
-
import test from "node:test"
|
|
2
|
+
import {describe, expect, it} from "@velocious/testing"
|
|
4
3
|
|
|
5
4
|
import {VirtualSocket} from "../../../src/client/virtual-socket.js"
|
|
6
5
|
|
|
@@ -27,7 +26,7 @@ function fakeConnection() {
|
|
|
27
26
|
_resumeReadable: () => { calls.resumed += 1 },
|
|
28
27
|
_deliverReadable: (/** @type {number} */ _id, /** @type {number} */ bytes) => { calls.delivered += bytes },
|
|
29
28
|
_acknowledgeReadable: (/** @type {number} */ _id, /** @type {number} */ bytes) => {
|
|
30
|
-
|
|
29
|
+
if (!(bytes <= calls.delivered - calls.acknowledged)) throw new Error("ACK exceeds delivered bytes")
|
|
31
30
|
calls.acknowledged += bytes
|
|
32
31
|
return Promise.resolve(calls.acknowledged)
|
|
33
32
|
},
|
|
@@ -42,179 +41,210 @@ function fakeConnection() {
|
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
43
|
|
|
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
|
-
test("VirtualSocket write backpressure emits drain and end emits finish", async () => {
|
|
71
|
-
const fake = fakeConnection()
|
|
72
|
-
const socket = new VirtualSocket(fake.connection, 3, {
|
|
73
|
-
host: "127.0.0.1",
|
|
74
|
-
port: 80,
|
|
75
|
-
writableHighWaterMark: 1,
|
|
76
|
-
allowHalfOpen: true
|
|
44
|
+
describe("VirtualSocket", () => {
|
|
45
|
+
it("VirtualSocket exposes normal connect metadata and compatibility methods", async () => {
|
|
46
|
+
const fake = fakeConnection()
|
|
47
|
+
const socket = new VirtualSocket(fake.connection, 1, {host: "target.internal", port: 443})
|
|
48
|
+
/** @type {string[]} */
|
|
49
|
+
const events = []
|
|
50
|
+
socket.on("connect", () => events.push("connect"))
|
|
51
|
+
socket.on("ready", () => events.push("ready"))
|
|
52
|
+
socket._markConnected()
|
|
53
|
+
|
|
54
|
+
expect(events).toEqual(["connect", "ready"])
|
|
55
|
+
expect(socket.connecting).toBe(false)
|
|
56
|
+
expect(socket.pending).toBe(false)
|
|
57
|
+
expect(socket.remoteAddress).toBe("target.internal")
|
|
58
|
+
expect(socket.remotePort).toBe(443)
|
|
59
|
+
expect(socket.address()).toEqual({address: "127.0.0.1", family: "IPv4", port: 43210})
|
|
60
|
+
expect(socket.setNoDelay()).toBe(socket)
|
|
61
|
+
expect(socket.setKeepAlive()).toBe(socket)
|
|
62
|
+
expect(socket.ref()).toBe(socket)
|
|
63
|
+
expect(socket.unref()).toBe(socket)
|
|
64
|
+
expect(fake.calls.refs).toBe(1)
|
|
65
|
+
expect(fake.calls.unrefs).toBe(1)
|
|
66
|
+
socket.destroy()
|
|
67
|
+
await new Promise((resolve) => socket.once("close", resolve))
|
|
77
68
|
})
|
|
78
|
-
socket._markConnected()
|
|
79
|
-
const drained = new Promise((resolve) => socket.once("drain", resolve))
|
|
80
|
-
assert.equal(socket.write("payload"), false)
|
|
81
|
-
await drained
|
|
82
|
-
assert.equal(socket.bytesWritten, 7)
|
|
83
|
-
const finished = new Promise((resolve) => socket.once("finish", resolve))
|
|
84
|
-
socket.end()
|
|
85
|
-
await finished
|
|
86
|
-
assert.equal(fake.calls.ended, 1)
|
|
87
|
-
socket.destroy()
|
|
88
|
-
})
|
|
89
69
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
})
|
|
70
|
+
it("VirtualSocket write backpressure emits drain and end emits finish", async () => {
|
|
71
|
+
const fake = fakeConnection()
|
|
72
|
+
const socket = new VirtualSocket(fake.connection, 3, {
|
|
73
|
+
host: "127.0.0.1",
|
|
74
|
+
port: 80,
|
|
75
|
+
writableHighWaterMark: 1,
|
|
76
|
+
allowHalfOpen: true
|
|
77
|
+
})
|
|
78
|
+
socket._markConnected()
|
|
79
|
+
const drained = new Promise((resolve) => socket.once("drain", resolve))
|
|
80
|
+
expect(socket.write("payload")).toBe(false)
|
|
81
|
+
await drained
|
|
82
|
+
expect(socket.bytesWritten).toBe(7)
|
|
83
|
+
const finished = new Promise((resolve) => socket.once("finish", resolve))
|
|
84
|
+
socket.end()
|
|
85
|
+
await finished
|
|
86
|
+
expect(fake.calls.ended).toBe(1)
|
|
87
|
+
socket.destroy()
|
|
88
|
+
})
|
|
109
89
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
90
|
+
it("VirtualSocket pause/resume, timeout, data and half-close remain stream-local", async () => {
|
|
91
|
+
const fake = fakeConnection()
|
|
92
|
+
const socket = new VirtualSocket(fake.connection, 5, {host: "127.0.0.1", port: 80, allowHalfOpen: true})
|
|
93
|
+
socket._markConnected()
|
|
94
|
+
socket.pause()
|
|
95
|
+
const timeout = new Promise((resolve) => socket.setTimeout(10, () => resolve(undefined)))
|
|
96
|
+
await timeout
|
|
97
|
+
socket.setTimeout(0)
|
|
98
|
+
const data = new Promise((resolve) => socket.once("data", (chunk) => resolve(chunk.toString())))
|
|
99
|
+
socket._receive(Buffer.from("hello"))
|
|
100
|
+
socket.resume()
|
|
101
|
+
expect(await data).toBe("hello")
|
|
102
|
+
expect(socket.bytesRead).toBe(5)
|
|
103
|
+
const ended = new Promise((resolve) => socket.once("end", resolve))
|
|
104
|
+
socket._receiveEnd()
|
|
105
|
+
await ended
|
|
106
|
+
expect(fake.calls.resumed > 0).toBeTruthy()
|
|
107
|
+
socket.destroy()
|
|
116
108
|
})
|
|
117
|
-
socket._markConnected()
|
|
118
109
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
110
|
+
it("VirtualSocket exposes opt-in manual receive ACK and durable write contracts", async () => {
|
|
111
|
+
const fake = fakeConnection()
|
|
112
|
+
const socket = new VirtualSocket(fake.connection, 7, {
|
|
113
|
+
host: "127.0.0.1",
|
|
114
|
+
port: 80,
|
|
115
|
+
manualReceiveAck: true
|
|
116
|
+
})
|
|
117
|
+
socket._markConnected()
|
|
118
|
+
|
|
119
|
+
const acknowledged = new Promise((resolve, reject) => {
|
|
120
|
+
socket.once("data", (chunk) => {
|
|
121
|
+
socket.acknowledgeReceive(chunk.length).then(resolve, reject)
|
|
122
|
+
})
|
|
122
123
|
})
|
|
124
|
+
socket._receive(Buffer.from("response"))
|
|
125
|
+
expect(await acknowledged).toBe(8)
|
|
126
|
+
expect(fake.calls.delivered).toBe(8)
|
|
127
|
+
expect(fake.calls.acknowledged).toBe(8)
|
|
128
|
+
|
|
129
|
+
await socket.writeDurable(new Uint8Array(Buffer.from("request")))
|
|
130
|
+
expect(fake.calls.durableWrites).toHaveLength(1)
|
|
131
|
+
const durableWrite = fake.calls.durableWrites[0]
|
|
132
|
+
expect(durableWrite).toBeDefined()
|
|
133
|
+
expect(durableWrite?.equals(Buffer.from("request"))).toBe(true)
|
|
134
|
+
expect(socket.bytesWritten).toBe(7)
|
|
135
|
+
// predicate-based check: thrown error must carry SOCKETDUCT_INVALID_RECEIVE_ACK
|
|
136
|
+
let error
|
|
137
|
+
try {
|
|
138
|
+
socket.acknowledgeReceive(1)
|
|
139
|
+
} catch (caught) {
|
|
140
|
+
error = caught
|
|
141
|
+
}
|
|
142
|
+
expect(error).toBeDefined()
|
|
143
|
+
expect(/** @type {{code?: string}} */ (error).code).toBe("SOCKETDUCT_INVALID_RECEIVE_ACK")
|
|
144
|
+
const closed = new Promise((resolve) => socket.once("close", resolve))
|
|
145
|
+
socket.destroy()
|
|
146
|
+
await closed
|
|
147
|
+
// predicate-based rejection: code ERR_STREAM_DESTROYED
|
|
148
|
+
let rejection
|
|
149
|
+
try {
|
|
150
|
+
await socket.acknowledgeReceive(1)
|
|
151
|
+
} catch (caught) {
|
|
152
|
+
rejection = caught
|
|
153
|
+
}
|
|
154
|
+
expect(rejection).toBeDefined()
|
|
155
|
+
expect(/** @type {{code?: string}} */ (rejection).code).toBe("ERR_STREAM_DESTROYED")
|
|
123
156
|
})
|
|
124
|
-
socket._receive(Buffer.from("response"))
|
|
125
|
-
assert.equal(await acknowledged, 8)
|
|
126
|
-
assert.equal(fake.calls.delivered, 8)
|
|
127
|
-
assert.equal(fake.calls.acknowledged, 8)
|
|
128
|
-
|
|
129
|
-
await socket.writeDurable(new Uint8Array(Buffer.from("request")))
|
|
130
|
-
assert.deepEqual(fake.calls.durableWrites, [Buffer.from("request")])
|
|
131
|
-
assert.equal(socket.bytesWritten, 7)
|
|
132
|
-
assert.throws(
|
|
133
|
-
() => socket.acknowledgeReceive(1),
|
|
134
|
-
(error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_INVALID_RECEIVE_ACK"
|
|
135
|
-
)
|
|
136
|
-
const closed = new Promise((resolve) => socket.once("close", resolve))
|
|
137
|
-
socket.destroy()
|
|
138
|
-
await closed
|
|
139
|
-
await assert.rejects(
|
|
140
|
-
socket.acknowledgeReceive(1),
|
|
141
|
-
(error) => /** @type {{code?: string}} */ (error).code === "ERR_STREAM_DESTROYED"
|
|
142
|
-
)
|
|
143
|
-
})
|
|
144
157
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
})
|
|
158
|
+
it("VirtualSocket orders durable writes behind queued ordinary writes", async () => {
|
|
159
|
+
const fake = fakeConnection()
|
|
160
|
+
/** @type {string[]} */
|
|
161
|
+
const delivered = []
|
|
162
|
+
/** @type {(() => void) | undefined} */
|
|
163
|
+
let releaseFirst
|
|
164
|
+
const connection = /** @type {{_writeStream: (id: number, data: Buffer, callback: (error?: Error | null) => void) => void, _writeDurableStream: (id: number, data: Buffer, callback: (error?: Error | null) => void) => void}} */ (/** @type {unknown} */ (fake.connection))
|
|
165
|
+
connection._writeStream = (_id, data, callback) => {
|
|
166
|
+
delivered.push(data.toString())
|
|
167
|
+
if (data.toString() === "A") releaseFirst = callback
|
|
168
|
+
else callback()
|
|
169
|
+
}
|
|
170
|
+
connection._writeDurableStream = (_id, data, callback) => {
|
|
171
|
+
delivered.push(data.toString())
|
|
172
|
+
callback()
|
|
173
|
+
}
|
|
174
|
+
const socket = new VirtualSocket(fake.connection, 9, {host: "127.0.0.1", port: 80})
|
|
175
|
+
|
|
176
|
+
const first = new Promise((resolve, reject) => socket.write("A", (error) => error ? reject(error) : resolve(undefined)))
|
|
177
|
+
const second = new Promise((resolve, reject) => socket.write("B", (error) => error ? reject(error) : resolve(undefined)))
|
|
178
|
+
const durable = socket.writeDurable("C")
|
|
179
|
+
if (releaseFirst === undefined) throw new Error("expected first write callback to be captured")
|
|
180
|
+
releaseFirst()
|
|
181
|
+
await Promise.all([first, second, durable])
|
|
182
|
+
|
|
183
|
+
expect(delivered).toEqual(["A", "B", "C"])
|
|
184
|
+
expect(socket.bytesWritten).toBe(3)
|
|
185
|
+
socket.destroy()
|
|
186
|
+
})
|
|
174
187
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
188
|
+
it("VirtualSocket rejects durable writes after end while ordinary writes remain queued", async () => {
|
|
189
|
+
const fake = fakeConnection()
|
|
190
|
+
/** @type {string[]} */
|
|
191
|
+
const delivered = []
|
|
192
|
+
/** @type {(() => void) | undefined} */
|
|
193
|
+
let releaseFirst
|
|
194
|
+
const connection = /** @type {{_writeStream: (id: number, data: Buffer, callback: (error?: Error | null) => void) => void}} */ (/** @type {unknown} */ (fake.connection))
|
|
195
|
+
connection._writeStream = (_id, data, callback) => {
|
|
196
|
+
delivered.push(data.toString())
|
|
197
|
+
if (data.toString() === "A") releaseFirst = callback
|
|
198
|
+
else callback()
|
|
199
|
+
}
|
|
200
|
+
const socket = new VirtualSocket(fake.connection, 11, {host: "127.0.0.1", port: 80})
|
|
201
|
+
|
|
202
|
+
const first = new Promise((resolve, reject) => socket.write("A", (error) => error ? reject(error) : resolve(undefined)))
|
|
203
|
+
const second = new Promise((resolve, reject) => socket.write("B", (error) => error ? reject(error) : resolve(undefined)))
|
|
204
|
+
const finished = new Promise((resolve) => socket.once("finish", resolve))
|
|
205
|
+
socket.end()
|
|
206
|
+
// predicate-based rejection: code ERR_STREAM_WRITE_AFTER_END
|
|
207
|
+
const rejected = (async () => {
|
|
208
|
+
try {
|
|
209
|
+
await socket.writeDurable("late")
|
|
210
|
+
throw new Error("expected writeDurable to reject")
|
|
211
|
+
} catch (caught) {
|
|
212
|
+
const code = /** @type {{code?: string}} */ (caught).code
|
|
213
|
+
if (code !== "ERR_STREAM_WRITE_AFTER_END") throw caught
|
|
214
|
+
}
|
|
215
|
+
})()
|
|
216
|
+
|
|
217
|
+
if (releaseFirst === undefined) throw new Error("expected first write callback to be captured")
|
|
218
|
+
releaseFirst()
|
|
219
|
+
await Promise.all([first, second, finished, rejected])
|
|
220
|
+
expect(delivered).toEqual(["A", "B"])
|
|
221
|
+
expect(fake.calls.durableWrites).toEqual([])
|
|
222
|
+
socket.destroy()
|
|
223
|
+
})
|
|
205
224
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
225
|
+
it("VirtualSocket keeps manual ACK opt-in and reports unsupported durable admission", async () => {
|
|
226
|
+
const fake = fakeConnection()
|
|
227
|
+
delete /** @type {{_writeDurableStream?: unknown}} */ (fake.connection)._writeDurableStream
|
|
228
|
+
const socket = new VirtualSocket(fake.connection, 9, {host: "127.0.0.1", port: 80})
|
|
229
|
+
|
|
230
|
+
// predicate-based check: thrown error must carry SOCKETDUCT_MANUAL_ACK_DISABLED
|
|
231
|
+
let error
|
|
232
|
+
try {
|
|
233
|
+
socket.acknowledgeReceive(1)
|
|
234
|
+
} catch (caught) {
|
|
235
|
+
error = caught
|
|
236
|
+
}
|
|
237
|
+
expect(error).toBeDefined()
|
|
238
|
+
expect(/** @type {{code?: string}} */ (error).code).toBe("SOCKETDUCT_MANUAL_ACK_DISABLED")
|
|
239
|
+
// predicate-based rejection: code SOCKETDUCT_DURABLE_WRITE_UNSUPPORTED
|
|
240
|
+
let rejection
|
|
241
|
+
try {
|
|
242
|
+
await socket.writeDurable("request")
|
|
243
|
+
} catch (caught) {
|
|
244
|
+
rejection = caught
|
|
245
|
+
}
|
|
246
|
+
expect(rejection).toBeDefined()
|
|
247
|
+
expect(/** @type {{code?: string}} */ (rejection).code).toBe("SOCKETDUCT_DURABLE_WRITE_UNSUPPORTED")
|
|
248
|
+
socket.destroy()
|
|
249
|
+
})
|
|
220
250
|
})
|