socketduct 0.1.25 → 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.
Files changed (56) hide show
  1. package/AGENTS.md +2 -2
  2. package/changelog.d/20261010130509-bound-log-frame-compression-memory.md +1 -0
  3. package/docs/runbooks/jsonl-integrity-rollout.md +8 -6
  4. package/package.json +2 -2
  5. package/scripts/test.js +9 -14
  6. package/src/logging/compressed-log-writer.js +1 -1
  7. package/src/logging/zstd-frame-codec.js +15 -1
  8. package/test/e2e/gateway-process.test.js +93 -92
  9. package/test/e2e/logger-shutdown-process.test.js +40 -37
  10. package/test/e2e/reverse-client-recovery-process.test.js +107 -103
  11. package/test/e2e/reverse-gateway-process.test.js +328 -318
  12. package/test/integration/initial-target-recovery.test.js +333 -334
  13. package/test/integration/logging-cli.test.js +121 -119
  14. package/test/integration/logging-compressed-recovery.test.js +96 -95
  15. package/test/integration/multiplexed-compatibility.test.js +178 -177
  16. package/test/integration/multiplexed-corrections.test.js +360 -359
  17. package/test/integration/multiplexed-failures.test.js +253 -251
  18. package/test/integration/multiplexed-transport-set.test.js +334 -322
  19. package/test/integration/multiplexed-transport.test.js +135 -134
  20. package/test/integration/reverse-gateway-stream-id-reset.test.js +90 -89
  21. package/test/integration/reverse-gateway.test.js +2309 -2218
  22. package/test/integration/reverse-resume-continuity.test.js +219 -218
  23. package/test/integration/reverse-route-class.test.js +358 -341
  24. package/test/integration/socketduct-http-agent.test.js +314 -312
  25. package/test/integration/socketduct-https-agent.test.js +516 -485
  26. package/test/integration/stream-compression.test.js +862 -824
  27. package/test/integration/tcp-forwarding.test.js +2550 -2447
  28. package/test/integration/tls-forwarding.test.js +593 -592
  29. package/test/unit/auth/auth.test.js +43 -44
  30. package/test/unit/auth/static-token-store.test.js +9 -8
  31. package/test/unit/client/connect.test.js +41 -40
  32. package/test/unit/client/deferred-virtual-socket.test.js +95 -94
  33. package/test/unit/client/multiplexed-spool-budget.test.js +155 -154
  34. package/test/unit/client/multiplexed-transport-set.test.js +830 -748
  35. package/test/unit/client/socketduct-socket.test.js +768 -771
  36. package/test/unit/client/virtual-socket.test.js +198 -168
  37. package/test/unit/config/docker-gateway-files.test.js +224 -224
  38. package/test/unit/config/environment.test.js +472 -492
  39. package/test/unit/index.test.js +53 -52
  40. package/test/unit/logging/compressed-log-worker.test.js +23 -22
  41. package/test/unit/logging/compressed-log-writer.test.js +155 -154
  42. package/test/unit/logging/log-retention.test.js +52 -47
  43. package/test/unit/logging/log-segment-index.test.js +84 -81
  44. package/test/unit/logging/zstd-frame-codec.test.js +28 -19
  45. package/test/unit/protocol/compression.test.js +254 -298
  46. package/test/unit/protocol/frame.test.js +280 -276
  47. package/test/unit/protocol/v3-compression.test.js +21 -23
  48. package/test/unit/protocol/v3-frame.test.js +42 -41
  49. package/test/unit/release-configuration.test.js +15 -12
  50. package/test/unit/reverse/disconnect-attribution.test.js +65 -64
  51. package/test/unit/reverse-terminal-outcomes.test.js +53 -50
  52. package/test/unit/server/control-handler.test.js +232 -231
  53. package/test/unit/server/primary.test.js +51 -49
  54. package/test/unit/server/relay-runtime.test.js +280 -294
  55. package/test/unit/sessions/session.test.js +48 -62
  56. 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
  })