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.
Files changed (58) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/changelog.d/20261025070000-reclaim-unreadable-retention-lock.md +14 -0
  4. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  5. package/package.json +2 -2
  6. package/scripts/test.js +9 -14
  7. package/src/logging/compressed-log-writer.js +1 -1
  8. package/src/logging/log-retention.js +36 -1
  9. package/src/logging/zstd-frame-codec.js +15 -1
  10. package/test/e2e/gateway-process.test.js +93 -92
  11. package/test/e2e/logger-shutdown-process.test.js +40 -37
  12. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  13. package/test/e2e/reverse-gateway-process.test.js +328 -318
  14. package/test/integration/initial-target-recovery.test.js +333 -334
  15. package/test/integration/logging-cli.test.js +121 -119
  16. package/test/integration/logging-compressed-recovery.test.js +96 -95
  17. package/test/integration/multiplexed-compatibility.test.js +178 -177
  18. package/test/integration/multiplexed-corrections.test.js +360 -359
  19. package/test/integration/multiplexed-failures.test.js +253 -251
  20. package/test/integration/multiplexed-transport-set.test.js +334 -322
  21. package/test/integration/multiplexed-transport.test.js +135 -134
  22. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  23. package/test/integration/reverse-gateway.test.js +2309 -2218
  24. package/test/integration/reverse-resume-continuity.test.js +219 -218
  25. package/test/integration/reverse-route-class.test.js +358 -341
  26. package/test/integration/socketduct-http-agent.test.js +314 -312
  27. package/test/integration/socketduct-https-agent.test.js +516 -485
  28. package/test/integration/stream-compression.test.js +862 -824
  29. package/test/integration/tcp-forwarding.test.js +2550 -2447
  30. package/test/integration/tls-forwarding.test.js +593 -592
  31. package/test/unit/auth/auth.test.js +43 -44
  32. package/test/unit/auth/static-token-store.test.js +9 -8
  33. package/test/unit/client/connect.test.js +41 -40
  34. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  35. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  36. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  37. package/test/unit/client/socketduct-socket.test.js +768 -771
  38. package/test/unit/client/virtual-socket.test.js +198 -168
  39. package/test/unit/config/docker-gateway-files.test.js +224 -224
  40. package/test/unit/config/environment.test.js +472 -492
  41. package/test/unit/index.test.js +53 -52
  42. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  43. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  44. package/test/unit/logging/log-retention.test.js +71 -48
  45. package/test/unit/logging/log-segment-index.test.js +84 -81
  46. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  47. package/test/unit/protocol/compression.test.js +254 -298
  48. package/test/unit/protocol/frame.test.js +280 -276
  49. package/test/unit/protocol/v3-compression.test.js +21 -23
  50. package/test/unit/protocol/v3-frame.test.js +42 -41
  51. package/test/unit/release-configuration.test.js +15 -12
  52. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  53. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  54. package/test/unit/server/control-handler.test.js +232 -231
  55. package/test/unit/server/primary.test.js +51 -49
  56. package/test/unit/server/relay-runtime.test.js +280 -294
  57. package/test/unit/sessions/session.test.js +48 -62
  58. 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 assert from "node:assert/strict"
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
- assert(bytes <= calls.delivered - calls.acknowledged)
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
- test("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
- assert.deepEqual(events, ["connect", "ready"])
55
- assert.equal(socket.connecting, false)
56
- assert.equal(socket.pending, false)
57
- assert.equal(socket.remoteAddress, "target.internal")
58
- assert.equal(socket.remotePort, 443)
59
- assert.deepEqual(socket.address(), {address: "127.0.0.1", family: "IPv4", port: 43210})
60
- assert.equal(socket.setNoDelay(), socket)
61
- assert.equal(socket.setKeepAlive(), socket)
62
- assert.equal(socket.ref(), socket)
63
- assert.equal(socket.unref(), socket)
64
- assert.equal(fake.calls.refs, 1)
65
- assert.equal(fake.calls.unrefs, 1)
66
- socket.destroy()
67
- await new Promise((resolve) => socket.once("close", resolve))
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
- test("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
- assert.equal(await data, "hello")
102
- assert.equal(socket.bytesRead, 5)
103
- const ended = new Promise((resolve) => socket.once("end", resolve))
104
- socket._receiveEnd()
105
- await ended
106
- assert(fake.calls.resumed > 0)
107
- socket.destroy()
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
- test("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
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
- const acknowledged = new Promise((resolve, reject) => {
120
- socket.once("data", (chunk) => {
121
- socket.acknowledgeReceive(chunk.length).then(resolve, reject)
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
- test("VirtualSocket orders durable writes behind queued ordinary writes", async () => {
146
- const fake = fakeConnection()
147
- /** @type {string[]} */
148
- const delivered = []
149
- /** @type {(() => void) | undefined} */
150
- let releaseFirst
151
- 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))
152
- connection._writeStream = (_id, data, callback) => {
153
- delivered.push(data.toString())
154
- if (data.toString() === "A") releaseFirst = callback
155
- else callback()
156
- }
157
- connection._writeDurableStream = (_id, data, callback) => {
158
- delivered.push(data.toString())
159
- callback()
160
- }
161
- const socket = new VirtualSocket(fake.connection, 9, {host: "127.0.0.1", port: 80})
162
-
163
- const first = new Promise((resolve, reject) => socket.write("A", (error) => error ? reject(error) : resolve(undefined)))
164
- const second = new Promise((resolve, reject) => socket.write("B", (error) => error ? reject(error) : resolve(undefined)))
165
- const durable = socket.writeDurable("C")
166
- assert(releaseFirst !== undefined)
167
- releaseFirst()
168
- await Promise.all([first, second, durable])
169
-
170
- assert.deepEqual(delivered, ["A", "B", "C"])
171
- assert.equal(socket.bytesWritten, 3)
172
- socket.destroy()
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
- test("VirtualSocket rejects durable writes after end while ordinary writes remain queued", async () => {
176
- const fake = fakeConnection()
177
- /** @type {string[]} */
178
- const delivered = []
179
- /** @type {(() => void) | undefined} */
180
- let releaseFirst
181
- const connection = /** @type {{_writeStream: (id: number, data: Buffer, callback: (error?: Error | null) => void) => void}} */ (/** @type {unknown} */ (fake.connection))
182
- connection._writeStream = (_id, data, callback) => {
183
- delivered.push(data.toString())
184
- if (data.toString() === "A") releaseFirst = callback
185
- else callback()
186
- }
187
- const socket = new VirtualSocket(fake.connection, 11, {host: "127.0.0.1", port: 80})
188
-
189
- const first = new Promise((resolve, reject) => socket.write("A", (error) => error ? reject(error) : resolve(undefined)))
190
- const second = new Promise((resolve, reject) => socket.write("B", (error) => error ? reject(error) : resolve(undefined)))
191
- const finished = new Promise((resolve) => socket.once("finish", resolve))
192
- socket.end()
193
- const rejected = assert.rejects(
194
- socket.writeDurable("late"),
195
- (error) => /** @type {{code?: string}} */ (error).code === "ERR_STREAM_WRITE_AFTER_END"
196
- )
197
-
198
- assert(releaseFirst !== undefined)
199
- releaseFirst()
200
- await Promise.all([first, second, finished, rejected])
201
- assert.deepEqual(delivered, ["A", "B"])
202
- assert.deepEqual(fake.calls.durableWrites, [])
203
- socket.destroy()
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
- test("VirtualSocket keeps manual ACK opt-in and reports unsupported durable admission", async () => {
207
- const fake = fakeConnection()
208
- delete /** @type {{_writeDurableStream?: unknown}} */ (fake.connection)._writeDurableStream
209
- const socket = new VirtualSocket(fake.connection, 9, {host: "127.0.0.1", port: 80})
210
-
211
- assert.throws(
212
- () => socket.acknowledgeReceive(1),
213
- (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_MANUAL_ACK_DISABLED"
214
- )
215
- await assert.rejects(
216
- socket.writeDurable("request"),
217
- (error) => /** @type {{code?: string}} */ (error).code === "SOCKETDUCT_DURABLE_WRITE_UNSUPPORTED"
218
- )
219
- socket.destroy()
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
  })