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,11 +1,10 @@
1
1
  /* eslint-disable jsdoc/escape-inline-tags, jsdoc/require-param, jsdoc/require-returns */
2
- import assert from "node:assert/strict"
2
+ import {describe, expect, it, withTestCleanup} from "@velocious/testing"
3
3
  import {EventEmitter} from "node:events"
4
4
  import {mkdtemp, rm} from "node:fs/promises"
5
5
  import {tmpdir} from "node:os"
6
6
  import {join} from "node:path"
7
7
  import {Duplex} from "node:stream"
8
- import test from "node:test"
9
8
 
10
9
  import {MultiplexedSocketductTransportSet} from "../../../src/client/multiplexed-transport-set.js"
11
10
  import {decodeV3Control, encodeV3Control, encodeV3Frame, V3FrameDecoder, V3FrameType} from "../../../src/protocol/v3-frame.js"
@@ -102,7 +101,7 @@ function deferred() {
102
101
  /** @template T @param {T[]} values - Values. @param {number} index - Index. @returns {T} */
103
102
  function at(values, index) {
104
103
  const value = values[index]
105
- assert(value !== undefined)
104
+ if (value === undefined) throw new Error(`expected a value at index ${index}`)
106
105
  return value
107
106
  }
108
107
 
@@ -145,165 +144,167 @@ async function settle() {
145
144
  await new Promise((resolve) => setImmediate(resolve))
146
145
  }
147
146
 
148
- test("destroy keeps shared spool capacity reserved until remove succeeds", async (t) => {
149
- const root = await mkdtemp(join(tmpdir(), "socketduct-destroy-spool-budget-"))
150
- const originalRemove = SessionSpool.prototype.remove
151
- const removeGate = deferred()
152
- const removeCalled = deferred()
153
- const removeFinished = deferred()
154
- SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
155
- if (identifier === "destroy_stream_01") {
156
- removeCalled.resolve()
157
- await removeGate.promise
147
+ describe("multiplexed spool budget", () => {
148
+ it("destroy keeps shared spool capacity reserved until remove succeeds", async () => withTestCleanup(async (after) => {
149
+ const root = await mkdtemp(join(tmpdir(), "socketduct-destroy-spool-budget-"))
150
+ const originalRemove = SessionSpool.prototype.remove
151
+ const removeGate = deferred()
152
+ const removeCalled = deferred()
153
+ const removeFinished = deferred()
154
+ SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
155
+ if (identifier === "destroy_stream_01") {
156
+ removeCalled.resolve()
157
+ await removeGate.promise
158
+ }
159
+ await originalRemove.call(this, identifier)
160
+ if (identifier === "destroy_stream_01") removeFinished.resolve()
158
161
  }
159
- await originalRemove.call(this, identifier)
160
- if (identifier === "destroy_stream_01") removeFinished.resolve()
161
- }
162
- const {set, controllers} = createFixture(root)
163
- t.after(async () => {
164
- removeGate.resolve()
165
- SessionSpool.prototype.remove = originalRemove
166
- await set.close().catch(() => {})
167
- await rm(root, {recursive: true, force: true})
168
- })
169
- await set.start()
170
- const first = set.createConnection({host: "target.internal", port: 443, identifier: "destroy_stream_01"})
171
- await onceEvent(first, "connect")
172
- assert.equal(await writeResult(first, 6), null)
173
- await at(controllers, 0).nextFrame()
174
- first.destroy()
175
- await removeCalled.promise
176
- await settle()
177
- assert.equal(set.status().budgets.dataBytes, 0)
178
- assert.equal(set.status().budgets.spoolBytes, 6)
162
+ const {set, controllers} = createFixture(root)
163
+ after(async () => {
164
+ removeGate.resolve()
165
+ SessionSpool.prototype.remove = originalRemove
166
+ await set.close().catch(() => {})
167
+ await rm(root, {recursive: true, force: true})
168
+ })
169
+ await set.start()
170
+ const first = set.createConnection({host: "target.internal", port: 443, identifier: "destroy_stream_01"})
171
+ await onceEvent(first, "connect")
172
+ expect(await writeResult(first, 6)).toBe(null)
173
+ await at(controllers, 0).nextFrame()
174
+ first.destroy()
175
+ await removeCalled.promise
176
+ await settle()
177
+ expect(set.status().budgets.dataBytes).toBe(0)
178
+ expect(set.status().budgets.spoolBytes).toBe(6)
179
179
 
180
- const sibling = set.createConnection({host: "target.internal", port: 443, identifier: "destroy_stream_02"})
181
- await onceEvent(sibling, "connect")
182
- const blocked = await writeResult(sibling, 3)
183
- let closeSettled = false
184
- const closing = set.close().then(() => { closeSettled = true })
185
- await settle()
186
- assert.equal(closeSettled, false)
187
- removeGate.resolve()
188
- await removeFinished.promise
189
- await closing
190
- assert.equal(/** @type {{code?: string}} */ (blocked).code, "SOCKETDUCT_SET_SPOOL_LIMIT")
191
- assert.equal(set.status().budgets.spoolBytes, 0)
192
- })
180
+ const sibling = set.createConnection({host: "target.internal", port: 443, identifier: "destroy_stream_02"})
181
+ await onceEvent(sibling, "connect")
182
+ const blocked = await writeResult(sibling, 3)
183
+ let closeSettled = false
184
+ const closing = set.close().then(() => { closeSettled = true })
185
+ await settle()
186
+ expect(closeSettled).toBe(false)
187
+ removeGate.resolve()
188
+ await removeFinished.promise
189
+ await closing
190
+ expect(/** @type {{code?: string}} */ (blocked).code).toBe("SOCKETDUCT_SET_SPOOL_LIMIT")
191
+ expect(set.status().budgets.spoolBytes).toBe(0)
192
+ }))
193
193
 
194
- test("failed destroy removal retains spool accounting and fails the member visibly", async (t) => {
195
- const root = await mkdtemp(join(tmpdir(), "socketduct-failed-remove-budget-"))
196
- const originalRemove = SessionSpool.prototype.remove
197
- const removeCalled = deferred()
198
- SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
199
- if (identifier === "failed_remove_stream_01") {
200
- removeCalled.resolve()
201
- throw new Error("injected remove failure")
194
+ it("failed destroy removal retains spool accounting and fails the member visibly", async () => withTestCleanup(async (after) => {
195
+ const root = await mkdtemp(join(tmpdir(), "socketduct-failed-remove-budget-"))
196
+ const originalRemove = SessionSpool.prototype.remove
197
+ const removeCalled = deferred()
198
+ SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
199
+ if (identifier === "failed_remove_stream_01") {
200
+ removeCalled.resolve()
201
+ throw new Error("injected remove failure")
202
+ }
203
+ await originalRemove.call(this, identifier)
202
204
  }
203
- await originalRemove.call(this, identifier)
204
- }
205
- const {set, controllers} = createFixture(root)
206
- t.after(async () => {
207
- SessionSpool.prototype.remove = originalRemove
208
- await set.close().catch(() => {})
209
- await rm(root, {recursive: true, force: true})
210
- })
211
- await set.start()
212
- const socket = set.createConnection({host: "target.internal", port: 443, identifier: "failed_remove_stream_01"})
213
- await onceEvent(socket, "connect")
214
- assert.equal(await writeResult(socket, 6), null)
215
- await at(controllers, 0).nextFrame()
216
- socket.destroy()
217
- await removeCalled.promise
218
- await settle()
219
- assert.equal(set.status().budgets.dataBytes, 0)
220
- assert.equal(set.status().budgets.spoolBytes, 6)
221
- assert.equal(at(set.status().members, 0).spoolCleanupFailures, 1)
222
- assert.equal(at(set.status().members, 0).state, "closed")
223
- })
205
+ const {set, controllers} = createFixture(root)
206
+ after(async () => {
207
+ SessionSpool.prototype.remove = originalRemove
208
+ await set.close().catch(() => {})
209
+ await rm(root, {recursive: true, force: true})
210
+ })
211
+ await set.start()
212
+ const socket = set.createConnection({host: "target.internal", port: 443, identifier: "failed_remove_stream_01"})
213
+ await onceEvent(socket, "connect")
214
+ expect(await writeResult(socket, 6)).toBe(null)
215
+ await at(controllers, 0).nextFrame()
216
+ socket.destroy()
217
+ await removeCalled.promise
218
+ await settle()
219
+ expect(set.status().budgets.dataBytes).toBe(0)
220
+ expect(set.status().budgets.spoolBytes).toBe(6)
221
+ expect(at(set.status().members, 0).spoolCleanupFailures).toBe(1)
222
+ expect(at(set.status().members, 0).state).toBe("closed")
223
+ }))
224
224
 
225
- test("ACK keeps shared spool capacity reserved until truncate succeeds", async (t) => {
226
- const root = await mkdtemp(join(tmpdir(), "socketduct-ack-spool-budget-"))
227
- const originalTruncate = SessionSpool.prototype.truncateAcked
228
- const truncateGate = deferred()
229
- const truncateCalled = deferred()
230
- const truncateFinished = deferred()
231
- SessionSpool.prototype.truncateAcked = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
232
- if (identifier === "ack_stream_01") {
233
- truncateCalled.resolve()
234
- await truncateGate.promise
225
+ it("ACK keeps shared spool capacity reserved until truncate succeeds", async () => withTestCleanup(async (after) => {
226
+ const root = await mkdtemp(join(tmpdir(), "socketduct-ack-spool-budget-"))
227
+ const originalTruncate = SessionSpool.prototype.truncateAcked
228
+ const truncateGate = deferred()
229
+ const truncateCalled = deferred()
230
+ const truncateFinished = deferred()
231
+ SessionSpool.prototype.truncateAcked = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
232
+ if (identifier === "ack_stream_01") {
233
+ truncateCalled.resolve()
234
+ await truncateGate.promise
235
+ }
236
+ await originalTruncate.call(this, identifier, sequence)
237
+ if (identifier === "ack_stream_01") truncateFinished.resolve()
235
238
  }
236
- await originalTruncate.call(this, identifier, sequence)
237
- if (identifier === "ack_stream_01") truncateFinished.resolve()
238
- }
239
- const {set, controllers} = createFixture(root)
240
- t.after(async () => {
241
- truncateGate.resolve()
242
- SessionSpool.prototype.truncateAcked = originalTruncate
243
- await set.close().catch(() => {})
244
- await rm(root, {recursive: true, force: true})
245
- })
246
- await set.start()
247
- const first = set.createConnection({host: "target.internal", port: 443, identifier: "ack_stream_01"})
248
- await onceEvent(first, "connect")
249
- assert.equal(await writeResult(first, 6), null)
250
- const sent = await at(controllers, 0).nextFrame()
251
- sent.socket.ack(sent.frame)
252
- await truncateCalled.promise
253
- await settle()
254
- assert.equal(set.status().budgets.dataBytes, 0)
255
- assert.equal(set.status().budgets.spoolBytes, 6)
239
+ const {set, controllers} = createFixture(root)
240
+ after(async () => {
241
+ truncateGate.resolve()
242
+ SessionSpool.prototype.truncateAcked = originalTruncate
243
+ await set.close().catch(() => {})
244
+ await rm(root, {recursive: true, force: true})
245
+ })
246
+ await set.start()
247
+ const first = set.createConnection({host: "target.internal", port: 443, identifier: "ack_stream_01"})
248
+ await onceEvent(first, "connect")
249
+ expect(await writeResult(first, 6)).toBe(null)
250
+ const sent = await at(controllers, 0).nextFrame()
251
+ sent.socket.ack(sent.frame)
252
+ await truncateCalled.promise
253
+ await settle()
254
+ expect(set.status().budgets.dataBytes).toBe(0)
255
+ expect(set.status().budgets.spoolBytes).toBe(6)
256
256
 
257
- const sibling = set.createConnection({host: "target.internal", port: 443, identifier: "ack_stream_02"})
258
- await onceEvent(sibling, "connect")
259
- const blocked = await writeResult(sibling, 3)
260
- truncateGate.resolve()
261
- await truncateFinished.promise
262
- await settle()
263
- assert.equal(/** @type {{code?: string}} */ (blocked).code, "SOCKETDUCT_SET_SPOOL_LIMIT")
264
- assert.equal(set.status().budgets.spoolBytes, 0)
265
- })
257
+ const sibling = set.createConnection({host: "target.internal", port: 443, identifier: "ack_stream_02"})
258
+ await onceEvent(sibling, "connect")
259
+ const blocked = await writeResult(sibling, 3)
260
+ truncateGate.resolve()
261
+ await truncateFinished.promise
262
+ await settle()
263
+ expect(/** @type {{code?: string}} */ (blocked).code).toBe("SOCKETDUCT_SET_SPOOL_LIMIT")
264
+ expect(set.status().budgets.spoolBytes).toBe(0)
265
+ }))
266
266
 
267
- test("failed ACK truncate and removal retain spool accounting and surface cleanup failure", async (t) => {
268
- const root = await mkdtemp(join(tmpdir(), "socketduct-failed-truncate-budget-"))
269
- const originalTruncate = SessionSpool.prototype.truncateAcked
270
- const originalRemove = SessionSpool.prototype.remove
271
- const truncateCalled = deferred()
272
- const removeCalled = deferred()
273
- SessionSpool.prototype.truncateAcked = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
274
- if (identifier === "failed_ack_stream_01") {
275
- truncateCalled.resolve()
276
- throw new Error("injected truncate failure")
267
+ it("failed ACK truncate and removal retain spool accounting and surface cleanup failure", async () => withTestCleanup(async (after) => {
268
+ const root = await mkdtemp(join(tmpdir(), "socketduct-failed-truncate-budget-"))
269
+ const originalTruncate = SessionSpool.prototype.truncateAcked
270
+ const originalRemove = SessionSpool.prototype.remove
271
+ const truncateCalled = deferred()
272
+ const removeCalled = deferred()
273
+ SessionSpool.prototype.truncateAcked = async function (/** @type {string} */ identifier, /** @type {number} */ sequence) {
274
+ if (identifier === "failed_ack_stream_01") {
275
+ truncateCalled.resolve()
276
+ throw new Error("injected truncate failure")
277
+ }
278
+ await originalTruncate.call(this, identifier, sequence)
277
279
  }
278
- await originalTruncate.call(this, identifier, sequence)
279
- }
280
- SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
281
- if (identifier === "failed_ack_stream_01") {
282
- removeCalled.resolve()
283
- throw new Error("injected remove failure")
280
+ SessionSpool.prototype.remove = async function (/** @type {string} */ identifier) {
281
+ if (identifier === "failed_ack_stream_01") {
282
+ removeCalled.resolve()
283
+ throw new Error("injected remove failure")
284
+ }
285
+ await originalRemove.call(this, identifier)
284
286
  }
285
- await originalRemove.call(this, identifier)
286
- }
287
- const {set, controllers} = createFixture(root)
288
- t.after(async () => {
289
- SessionSpool.prototype.truncateAcked = originalTruncate
290
- SessionSpool.prototype.remove = originalRemove
291
- await set.close().catch(() => {})
292
- await rm(root, {recursive: true, force: true})
293
- })
294
- await set.start()
295
- const socket = set.createConnection({host: "target.internal", port: 443, identifier: "failed_ack_stream_01"})
296
- await onceEvent(socket, "connect")
297
- assert.equal(await writeResult(socket, 6), null)
298
- const sent = await at(controllers, 0).nextFrame()
299
- const streamError = onceEvent(socket, "error").catch((error) => error)
300
- sent.socket.ack(sent.frame)
301
- await truncateCalled.promise
302
- await removeCalled.promise
303
- await streamError
304
- await settle()
305
- assert.equal(set.status().budgets.dataBytes, 0)
306
- assert.equal(set.status().budgets.spoolBytes, 6)
307
- assert.equal(at(set.status().members, 0).spoolCleanupFailures, 1)
308
- assert.equal(at(set.status().members, 0).state, "closed")
287
+ const {set, controllers} = createFixture(root)
288
+ after(async () => {
289
+ SessionSpool.prototype.truncateAcked = originalTruncate
290
+ SessionSpool.prototype.remove = originalRemove
291
+ await set.close().catch(() => {})
292
+ await rm(root, {recursive: true, force: true})
293
+ })
294
+ await set.start()
295
+ const socket = set.createConnection({host: "target.internal", port: 443, identifier: "failed_ack_stream_01"})
296
+ await onceEvent(socket, "connect")
297
+ expect(await writeResult(socket, 6)).toBe(null)
298
+ const sent = await at(controllers, 0).nextFrame()
299
+ const streamError = onceEvent(socket, "error").catch((error) => error)
300
+ sent.socket.ack(sent.frame)
301
+ await truncateCalled.promise
302
+ await removeCalled.promise
303
+ await streamError
304
+ await settle()
305
+ expect(set.status().budgets.dataBytes).toBe(0)
306
+ expect(set.status().budgets.spoolBytes).toBe(6)
307
+ expect(at(set.status().members, 0).spoolCleanupFailures).toBe(1)
308
+ expect(at(set.status().members, 0).state).toBe("closed")
309
+ }))
309
310
  })