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