socketduct 0.1.15

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 (138) hide show
  1. package/.dockerignore +11 -0
  2. package/.env.example +131 -0
  3. package/AGENTS.md +168 -0
  4. package/Dockerfile +30 -0
  5. package/README.md +134 -0
  6. package/changelog.d/20260716083144-v2-ack-wire-format.md +3 -0
  7. package/changelog.d/20260716092044-v2-only-stream-framing.md +1 -0
  8. package/changelog.d/20260727113000-relay-connect-timeout-destroy.md +1 -0
  9. package/changelog.d/20260813093000-private-relay-hardening.md +1 -0
  10. package/changelog.d/20260824183412-initial-target-recovery-https.md +1 -0
  11. package/changelog.d/20260824230000-tls-relay-session-close.md +1 -0
  12. package/changelog.d/20260825120000-reconnect-close-target-timeout.md +1 -0
  13. package/changelog.d/20260829120000-protocol-v3-multiplexing.md +1 -0
  14. package/changelog.d/20260829160000-v3-review-corrections.md +1 -0
  15. package/changelog.d/20260830000000-fix-socket-idle-timeout.md +1 -0
  16. package/changelog.d/20260830090000-multi-gateway-transport-set.md +1 -0
  17. package/changelog.d/20260830093000-relay-close-settlement.md +1 -0
  18. package/changelog.d/20260830120000-immutable-gateway-image.md +1 -0
  19. package/changelog.d/20260902210000-native-reverse-gateway.md +1 -0
  20. package/changelog.d/20260902230000-relay-resume-terminal-order.md +1 -0
  21. package/changelog.d/20260902230708-multiplexed-open-ownership.md +1 -0
  22. package/changelog.d/20260902231541-multiplexed-terminal-close.md +1 -0
  23. package/changelog.d/20260902231542-reverse-close-position.md +1 -0
  24. package/changelog.d/20260902231543-reverse-terminal-connect.md +1 -0
  25. package/changelog.d/20260902233000-reverse-gateway-bounds.md +1 -0
  26. package/changelog.d/20260903001000-reverse-data-ordering.md +1 -0
  27. package/changelog.d/20260903003000-reverse-hub-restart-window.md +1 -0
  28. package/changelog.d/20260903010000-reverse-terminal-lifecycle.md +1 -0
  29. package/changelog.d/20260904142743-transport-set-readiness-wait.md +1 -0
  30. package/changelog.d/20260904152855-deferred-open-snapshots-binary-views.md +1 -0
  31. package/changelog.d/20260904163041-tensorbuzz-container-workflow.md +1 -0
  32. package/changelog.d/20260904174955-transport-set-terminal-causes.md +1 -0
  33. package/changelog.d/20260905081000-contain-reverse-auth-write-errors.md +1 -0
  34. package/changelog.d/20260905100520-reverse-hub-close-race.md +1 -0
  35. package/changelog.d/20260905120000-persistent-jsonl-logging.md +1 -0
  36. package/changelog.d/20260908071009-jsonl-partial-append-recovery.md +1 -0
  37. package/changelog.d/20260908074916-private-log-directory-setup.md +1 -0
  38. package/changelog.d/20260909093057-reverse-large-write-backpressure.md +1 -0
  39. package/changelog.d/20260909135830-actionable-error-diagnostics.md +1 -0
  40. package/changelog.d/20260909150000-transport-set-error-redaction.md +1 -0
  41. package/docker-compose.dev.yml +19 -0
  42. package/docker-compose.reverse-gateway.yml +36 -0
  43. package/docker-compose.yml +73 -0
  44. package/docs/architecture.md +264 -0
  45. package/docs/docker-topology.md +164 -0
  46. package/docs/docker.md +281 -0
  47. package/docs/logging.md +185 -0
  48. package/docs/plans/initial-implementation.md +205 -0
  49. package/docs/plans/socket-docker-integration.md +221 -0
  50. package/docs/protocol.md +558 -0
  51. package/docs/releasing.md +18 -0
  52. package/docs/reverse-gateway.md +186 -0
  53. package/docs/runbooks/jsonl-integrity-rollout.md +144 -0
  54. package/docs/running.md +215 -0
  55. package/docs/security.md +258 -0
  56. package/docs/session-lifecycle.md +127 -0
  57. package/docs/socket-api.md +448 -0
  58. package/eslint.config.js +36 -0
  59. package/package.json +44 -0
  60. package/scripts/deploy-gateway.js +194 -0
  61. package/scripts/test.js +28 -0
  62. package/src/auth/passwords.js +38 -0
  63. package/src/auth/static-token-store.js +41 -0
  64. package/src/auth/user-store.js +139 -0
  65. package/src/client/connect.js +76 -0
  66. package/src/client/deferred-virtual-socket.js +312 -0
  67. package/src/client/http-agent.js +101 -0
  68. package/src/client/https-agent.js +366 -0
  69. package/src/client/multiplexed-connection.js +1504 -0
  70. package/src/client/multiplexed-transport-set.js +917 -0
  71. package/src/client/multiplexed.js +3 -0
  72. package/src/client/socketduct-socket.js +1174 -0
  73. package/src/client/virtual-socket.js +256 -0
  74. package/src/config/environment.js +400 -0
  75. package/src/edge/tcp-edge.js +82 -0
  76. package/src/index.js +101 -0
  77. package/src/logging/structured-logger.js +745 -0
  78. package/src/protocol/compression.js +325 -0
  79. package/src/protocol/frame.js +341 -0
  80. package/src/protocol/v3-compression.js +44 -0
  81. package/src/protocol/v3-frame.js +209 -0
  82. package/src/proxy/multiplexed-relay.js +798 -0
  83. package/src/proxy/tcp-relay.js +1191 -0
  84. package/src/relay.js +3 -0
  85. package/src/reverse/client.js +721 -0
  86. package/src/reverse/common.js +88 -0
  87. package/src/reverse/gateway.js +663 -0
  88. package/src/reverse/hub.js +563 -0
  89. package/src/reverse/index.js +3 -0
  90. package/src/reverse/runtime.js +78 -0
  91. package/src/server/control-handler.js +170 -0
  92. package/src/server/primary.js +64 -0
  93. package/src/server/relay-runtime.js +124 -0
  94. package/src/server/worker.js +39 -0
  95. package/src/sessions/session.js +212 -0
  96. package/src/spool/spool-file.js +451 -0
  97. package/tensorbuzz.yml +36 -0
  98. package/test/e2e/gateway-process.test.js +262 -0
  99. package/test/e2e/reverse-gateway-process.test.js +285 -0
  100. package/test/fixtures/logging/enospc-matrix.mjs +238 -0
  101. package/test/fixtures/logging/enospc-recovery.mjs +146 -0
  102. package/test/fixtures/logging/process-writer.mjs +20 -0
  103. package/test/helpers/tls-certificates.js +115 -0
  104. package/test/integration/initial-target-recovery.test.js +518 -0
  105. package/test/integration/logging-recovery.test.js +163 -0
  106. package/test/integration/multiplexed-compatibility.test.js +254 -0
  107. package/test/integration/multiplexed-corrections.test.js +551 -0
  108. package/test/integration/multiplexed-failures.test.js +339 -0
  109. package/test/integration/multiplexed-transport-set.test.js +456 -0
  110. package/test/integration/multiplexed-transport.test.js +208 -0
  111. package/test/integration/reverse-gateway.test.js +1293 -0
  112. package/test/integration/socketduct-http-agent.test.js +435 -0
  113. package/test/integration/socketduct-https-agent.test.js +669 -0
  114. package/test/integration/stream-compression.test.js +1267 -0
  115. package/test/integration/tcp-forwarding.test.js +2878 -0
  116. package/test/integration/tls-forwarding.test.js +748 -0
  117. package/test/unit/auth/auth.test.js +64 -0
  118. package/test/unit/auth/static-token-store.test.js +14 -0
  119. package/test/unit/client/connect.test.js +58 -0
  120. package/test/unit/client/multiplexed-spool-budget.test.js +309 -0
  121. package/test/unit/client/multiplexed-transport-set.test.js +906 -0
  122. package/test/unit/client/socketduct-socket.test.js +1124 -0
  123. package/test/unit/client/virtual-socket.test.js +98 -0
  124. package/test/unit/config/docker-gateway-files.test.js +274 -0
  125. package/test/unit/config/environment.test.js +507 -0
  126. package/test/unit/index.test.js +78 -0
  127. package/test/unit/logging/structured-logger.test.js +674 -0
  128. package/test/unit/protocol/compression.test.js +421 -0
  129. package/test/unit/protocol/frame.test.js +336 -0
  130. package/test/unit/protocol/v3-compression.test.js +31 -0
  131. package/test/unit/protocol/v3-frame.test.js +57 -0
  132. package/test/unit/release-configuration.test.js +21 -0
  133. package/test/unit/server/control-handler.test.js +291 -0
  134. package/test/unit/server/primary.test.js +59 -0
  135. package/test/unit/server/relay-runtime.test.js +434 -0
  136. package/test/unit/sessions/session.test.js +84 -0
  137. package/test/unit/spool/spool-file.test.js +295 -0
  138. package/tsconfig.json +20 -0
@@ -0,0 +1,421 @@
1
+ import assert from "node:assert/strict"
2
+ import {randomBytes} from "node:crypto"
3
+ import test from "node:test"
4
+ import zlib from "node:zlib"
5
+
6
+ import {
7
+ AdaptiveCompressor,
8
+ availableCodecs,
9
+ CompressionProtocolError,
10
+ decodeStreamPayload,
11
+ encodeStreamPayload,
12
+ negotiateCodec,
13
+ zstdAvailable,
14
+ ZSTD_CODEC
15
+ } from "../../../src/protocol/compression.js"
16
+
17
+ /**
18
+ * @param {Buffer} input - Raw bytes.
19
+ * @param {object} [options] - zstd options.
20
+ * @returns {Promise<Buffer>} - Compressed bytes.
21
+ */
22
+ function zstdCompress(input, options = {}) {
23
+ return new Promise((resolve, reject) => {
24
+ zlib.zstdCompress(input, options, (err, result) => {
25
+ if (err) { reject(err); return }
26
+ resolve(result)
27
+ })
28
+ })
29
+ }
30
+
31
+ /**
32
+ * @param {number} size - Requested size in bytes.
33
+ * @returns {Buffer} - Highly compressible text payload.
34
+ */
35
+ function compressibleChunk(size) {
36
+ return Buffer.from("GET /api/v1/users HTTP/1.1\r\nHost: example.com\r\nAccept: application/json\r\n\r\n".repeat(200)).subarray(0, size)
37
+ }
38
+
39
+ /**
40
+ * @param {number} size - Requested size in bytes.
41
+ * @returns {Buffer} - Incompressible random payload.
42
+ */
43
+ function incompressibleChunk(size) {
44
+ return randomBytes(size)
45
+ }
46
+
47
+ test("availableCodecs advertises zstd only when the runtime supports it", () => {
48
+ assert.deepEqual(availableCodecs(), zstdAvailable() ? [ZSTD_CODEC] : [])
49
+ })
50
+
51
+ test("negotiateCodec selects the first mutually supported codec", () => {
52
+ assert.equal(negotiateCodec(["zstd"], ["zstd"]), "zstd")
53
+ assert.equal(negotiateCodec(["brotli", "zstd"], ["zstd"]), "zstd")
54
+ })
55
+
56
+ test("negotiateCodec returns undefined for absent, empty, unknown or malformed offers", () => {
57
+ assert.equal(negotiateCodec(undefined, ["zstd"]), undefined)
58
+ assert.equal(negotiateCodec([], ["zstd"]), undefined)
59
+ assert.equal(negotiateCodec(["brotli"], ["zstd"]), undefined)
60
+ assert.equal(negotiateCodec("zstd", ["zstd"]), undefined)
61
+ assert.equal(negotiateCodec({codec: "zstd"}, ["zstd"]), undefined)
62
+ assert.equal(negotiateCodec(["zstd"], []), undefined)
63
+ })
64
+
65
+ test("encodeStreamPayload round-trips a compressible payload and shrinks it", {skip: !zstdAvailable()}, async () => {
66
+ const chunk = compressibleChunk(8192)
67
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
68
+
69
+ assert.equal(encoded.encoding, ZSTD_CODEC)
70
+ assert.equal(encoded.dataLength, chunk.length)
71
+ assert.ok(encoded.payload.length < chunk.length)
72
+
73
+ const decoded = await decodeStreamPayload(
74
+ {type: "stream.data", encoding: encoded.encoding, dataLength: encoded.dataLength},
75
+ encoded.payload,
76
+ ZSTD_CODEC
77
+ )
78
+ assert.deepEqual(decoded, chunk)
79
+ })
80
+
81
+ test("encodeStreamPayload sends raw when no codec is negotiated", async () => {
82
+ const chunk = compressibleChunk(8192)
83
+ const encoded = await encodeStreamPayload(chunk, undefined)
84
+
85
+ assert.equal(encoded.encoding, undefined)
86
+ assert.equal(encoded.dataLength, undefined)
87
+ assert.deepEqual(encoded.payload, chunk)
88
+ })
89
+
90
+ test("encodeStreamPayload sends payloads below the minimum size raw", {skip: !zstdAvailable()}, async () => {
91
+ const chunk = compressibleChunk(64)
92
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
93
+
94
+ assert.equal(encoded.encoding, undefined)
95
+ assert.deepEqual(encoded.payload, chunk)
96
+ })
97
+
98
+ test("encodeStreamPayload never grows a frame for incompressible data", {skip: !zstdAvailable()}, async () => {
99
+ const chunk = incompressibleChunk(16384)
100
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
101
+
102
+ assert.equal(encoded.encoding, undefined)
103
+ assert.ok(encoded.payload.length <= chunk.length)
104
+ assert.deepEqual(encoded.payload, chunk)
105
+ })
106
+
107
+ test("decodeStreamPayload passes raw payloads through untouched", async () => {
108
+ const chunk = compressibleChunk(1024)
109
+ assert.deepEqual(await decodeStreamPayload({type: "stream.data"}, chunk, ZSTD_CODEC), chunk)
110
+ assert.deepEqual(await decodeStreamPayload({type: "stream.data"}, chunk, undefined), chunk)
111
+ })
112
+
113
+ test("decodeStreamPayload enforces the uncompressed maximum on raw payloads too", async () => {
114
+ const chunk = compressibleChunk(4096)
115
+
116
+ await assert.rejects(
117
+ () => decodeStreamPayload({type: "stream.data"}, chunk, undefined, {maxDecompressedBytes: 1024}),
118
+ CompressionProtocolError
119
+ )
120
+ await assert.rejects(
121
+ () => decodeStreamPayload({type: "stream.data"}, chunk, ZSTD_CODEC, {maxDecompressedBytes: 1024}),
122
+ CompressionProtocolError
123
+ )
124
+ })
125
+
126
+ test("decodeStreamPayload still passes a raw payload within the uncompressed maximum", async () => {
127
+ const chunk = compressibleChunk(1024)
128
+
129
+ assert.deepEqual(await decodeStreamPayload({type: "stream.data"}, chunk, undefined, {maxDecompressedBytes: 1024}), chunk)
130
+ })
131
+
132
+ test("decodeStreamPayload rejects dataLength without an encoding", async () => {
133
+ await assert.rejects(
134
+ () => decodeStreamPayload({type: "stream.data", dataLength: 10}, Buffer.alloc(4), ZSTD_CODEC),
135
+ CompressionProtocolError
136
+ )
137
+ })
138
+
139
+ test("decodeStreamPayload rejects an encoding that was never negotiated", {skip: !zstdAvailable()}, async () => {
140
+ const chunk = compressibleChunk(4096)
141
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
142
+
143
+ await assert.rejects(
144
+ () => decodeStreamPayload({type: "stream.data", encoding: "zstd", dataLength: chunk.length}, encoded.payload, undefined),
145
+ CompressionProtocolError
146
+ )
147
+ })
148
+
149
+ test("decodeStreamPayload rejects an unknown or non-string encoding", async () => {
150
+ await assert.rejects(
151
+ () => decodeStreamPayload({type: "stream.data", encoding: "brotli", dataLength: 32}, Buffer.alloc(8), ZSTD_CODEC),
152
+ CompressionProtocolError
153
+ )
154
+ await assert.rejects(
155
+ () => decodeStreamPayload({type: "stream.data", encoding: 7, dataLength: 32}, Buffer.alloc(8), ZSTD_CODEC),
156
+ CompressionProtocolError
157
+ )
158
+ })
159
+
160
+ test("decodeStreamPayload rejects malformed dataLength values", {skip: !zstdAvailable()}, async () => {
161
+ const chunk = compressibleChunk(4096)
162
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
163
+
164
+ for (const dataLength of [undefined, 0, -1, 1.5, "4096", Number.MAX_SAFE_INTEGER + 2]) {
165
+ await assert.rejects(
166
+ () => decodeStreamPayload({type: "stream.data", encoding: ZSTD_CODEC, dataLength}, encoded.payload, ZSTD_CODEC),
167
+ CompressionProtocolError
168
+ )
169
+ }
170
+ })
171
+
172
+ test("decodeStreamPayload rejects a dataLength above the configured maximum", {skip: !zstdAvailable()}, async () => {
173
+ const chunk = compressibleChunk(4096)
174
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
175
+
176
+ await assert.rejects(
177
+ () => decodeStreamPayload(
178
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: chunk.length},
179
+ encoded.payload,
180
+ ZSTD_CODEC,
181
+ {maxDecompressedBytes: 1024}
182
+ ),
183
+ CompressionProtocolError
184
+ )
185
+ })
186
+
187
+ test("decodeStreamPayload rejects a compressed payload above the configured maximum", {skip: !zstdAvailable()}, async () => {
188
+ const chunk = compressibleChunk(4096)
189
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
190
+
191
+ await assert.rejects(
192
+ () => decodeStreamPayload(
193
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: chunk.length},
194
+ encoded.payload,
195
+ ZSTD_CODEC,
196
+ {maxCompressedBytes: 8}
197
+ ),
198
+ CompressionProtocolError
199
+ )
200
+ })
201
+
202
+ test("decodeStreamPayload rejects an empty compressed payload", async () => {
203
+ await assert.rejects(
204
+ () => decodeStreamPayload({type: "stream.data", encoding: ZSTD_CODEC, dataLength: 16}, Buffer.alloc(0), ZSTD_CODEC),
205
+ CompressionProtocolError
206
+ )
207
+ })
208
+
209
+ test("decodeStreamPayload rejects payloads whose expansion ratio is too high", {skip: !zstdAvailable()}, async () => {
210
+ const bomb = Buffer.alloc(4 * 1024 * 1024, 0)
211
+ const compressed = await zstdCompress(bomb)
212
+
213
+ await assert.rejects(
214
+ () => decodeStreamPayload(
215
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: bomb.length},
216
+ compressed,
217
+ ZSTD_CODEC
218
+ ),
219
+ CompressionProtocolError
220
+ )
221
+ })
222
+
223
+ test("decodeStreamPayload refuses to expand a bomb beyond its advertised dataLength", {skip: !zstdAvailable()}, async () => {
224
+ const bomb = Buffer.alloc(4 * 1024 * 1024, 0)
225
+ const compressed = await zstdCompress(bomb)
226
+
227
+ // Understates the true size: decompression must be bounded, not allocate 4 MiB.
228
+ await assert.rejects(
229
+ () => decodeStreamPayload(
230
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: 1024},
231
+ compressed,
232
+ ZSTD_CODEC,
233
+ {maxExpansionRatio: Number.MAX_SAFE_INTEGER}
234
+ ),
235
+ CompressionProtocolError
236
+ )
237
+ })
238
+
239
+ test("decodeStreamPayload rejects a dataLength larger than the true decompressed size", {skip: !zstdAvailable()}, async () => {
240
+ const chunk = compressibleChunk(4096)
241
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
242
+
243
+ await assert.rejects(
244
+ () => decodeStreamPayload(
245
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: chunk.length + 1},
246
+ encoded.payload,
247
+ ZSTD_CODEC
248
+ ),
249
+ CompressionProtocolError
250
+ )
251
+ })
252
+
253
+ test("decodeStreamPayload rejects a corrupt compressed payload", {skip: !zstdAvailable()}, async () => {
254
+ const chunk = compressibleChunk(4096)
255
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
256
+ const corrupted = Buffer.from(encoded.payload)
257
+ corrupted.writeUInt8(corrupted.readUInt8(corrupted.length - 1) ^ 0xff, corrupted.length - 1)
258
+ corrupted.writeUInt8(corrupted.readUInt8(10) ^ 0xff, 10)
259
+
260
+ await assert.rejects(
261
+ () => decodeStreamPayload(
262
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: chunk.length},
263
+ corrupted,
264
+ ZSTD_CODEC
265
+ ),
266
+ CompressionProtocolError
267
+ )
268
+ })
269
+
270
+ test("decodeStreamPayload rejects a truncated compressed payload", {skip: !zstdAvailable()}, async () => {
271
+ const chunk = compressibleChunk(4096)
272
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
273
+
274
+ await assert.rejects(
275
+ () => decodeStreamPayload(
276
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: chunk.length},
277
+ encoded.payload.subarray(0, encoded.payload.length - 4),
278
+ ZSTD_CODEC
279
+ ),
280
+ CompressionProtocolError
281
+ )
282
+ })
283
+
284
+ test("AdaptiveCompressor backs off on incompressible streams and re-probes", () => {
285
+ const adaptive = new AdaptiveCompressor()
286
+
287
+ assert.equal(adaptive.shouldCompress(), true)
288
+ adaptive.record(1.0)
289
+
290
+ // Backs off for at least one chunk after an incompressible probe.
291
+ assert.equal(adaptive.shouldCompress(), false)
292
+ // Then re-probes.
293
+ assert.equal(adaptive.shouldCompress(), true)
294
+ })
295
+
296
+ test("AdaptiveCompressor keeps compressing while the stream compresses well", () => {
297
+ const adaptive = new AdaptiveCompressor()
298
+
299
+ for (let index = 0; index < 5; index += 1) {
300
+ assert.equal(adaptive.shouldCompress(), true)
301
+ adaptive.record(0.3)
302
+ }
303
+ })
304
+
305
+ test("AdaptiveCompressor widens the skip window as incompressible chunks continue", () => {
306
+ const adaptive = new AdaptiveCompressor()
307
+ let skipped = 0
308
+
309
+ for (let round = 0; round < 4; round += 1) {
310
+ while (!adaptive.shouldCompress()) {
311
+ skipped += 1
312
+ }
313
+ adaptive.record(1.0)
314
+ }
315
+
316
+ // 1 + 2 + 4 skipped chunks across the three back-off rounds that follow a probe.
317
+ assert.equal(skipped, 7)
318
+ })
319
+
320
+ test("encodeStreamPayload skips compression while the adaptive window is closed", {skip: !zstdAvailable()}, async () => {
321
+ const adaptive = new AdaptiveCompressor()
322
+ const incompressible = incompressibleChunk(8192)
323
+ const compressible = compressibleChunk(8192)
324
+
325
+ // First probe is incompressible, which closes the window.
326
+ assert.equal((await encodeStreamPayload(incompressible, ZSTD_CODEC, adaptive)).encoding, undefined)
327
+ // Next chunk is skipped without spending CPU, even though it would compress well.
328
+ assert.equal((await encodeStreamPayload(compressible, ZSTD_CODEC, adaptive)).encoding, undefined)
329
+ // The re-probe compresses it.
330
+ assert.equal((await encodeStreamPayload(compressible, ZSTD_CODEC, adaptive)).encoding, ZSTD_CODEC)
331
+ })
332
+
333
+ test("encodeStreamPayload yields to the event loop and does not block synchronously", {skip: !zstdAvailable()}, async () => {
334
+ const chunk = compressibleChunk(16384)
335
+
336
+ // Two concurrent encode calls must both complete with correct results.
337
+ // The second call must not be blocked by the first.
338
+ const [encoded1, encoded2] = await Promise.all([
339
+ encodeStreamPayload(chunk, ZSTD_CODEC),
340
+ encodeStreamPayload(chunk, ZSTD_CODEC)
341
+ ])
342
+
343
+ assert.equal(encoded1.encoding, ZSTD_CODEC)
344
+ assert.ok(encoded1.payload.length < chunk.length)
345
+ assert.equal(encoded2.encoding, ZSTD_CODEC)
346
+ assert.ok(encoded2.payload.length < chunk.length)
347
+ // Independent frames produce independent compressed representations.
348
+ assert.deepEqual(Buffer.concat([encoded1.payload, encoded2.payload]).length < chunk.length * 2, true)
349
+ })
350
+
351
+ test("decodeStreamPayload yields to the event loop and does not block synchronously", {skip: !zstdAvailable()}, async () => {
352
+ const chunk = compressibleChunk(8192)
353
+ const encoded = await encodeStreamPayload(chunk, ZSTD_CODEC)
354
+ const header = {type: "stream.data", encoding: ZSTD_CODEC, dataLength: chunk.length}
355
+
356
+ // Two concurrent decode calls must both complete without blocking each other.
357
+ const [decoded1, decoded2] = await Promise.all([
358
+ decodeStreamPayload(header, encoded.payload, ZSTD_CODEC),
359
+ decodeStreamPayload(header, encoded.payload, ZSTD_CODEC)
360
+ ])
361
+
362
+ assert.deepEqual(decoded1, chunk)
363
+ assert.deepEqual(decoded2, chunk)
364
+ })
365
+
366
+ test("async zstd encoding preserves frame ordering when serialized", {skip: !zstdAvailable()}, async () => {
367
+ const chunks = [
368
+ compressibleChunk(4096),
369
+ compressibleChunk(8192),
370
+ compressibleChunk(16384)
371
+ ]
372
+
373
+ // Sequential encoding: each completes fully before the next starts.
374
+ // The test proves the async API returns in order when awaited in sequence.
375
+ const frames = []
376
+ for (const chunk of chunks) {
377
+ frames.push({chunk, encoded: await encodeStreamPayload(chunk, ZSTD_CODEC)})
378
+ }
379
+
380
+ assert.equal(frames.length, 3)
381
+ for (const frame of frames) {
382
+ assert.equal(frame.encoded.encoding, ZSTD_CODEC)
383
+ assert.equal(frame.encoded.dataLength, frame.chunk.length)
384
+ }
385
+ // Decode each back and verify data integrity.
386
+ for (const frame of frames) {
387
+ const decoded = await decodeStreamPayload(
388
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: frame.chunk.length},
389
+ frame.encoded.payload,
390
+ ZSTD_CODEC
391
+ )
392
+ assert.deepEqual(decoded, frame.chunk)
393
+ }
394
+ })
395
+
396
+ test("async zstd encode-decode round-trips independent payloads without leaking state between calls", {skip: !zstdAvailable()}, async () => {
397
+ // Interleave encoding and decoding to prove each zstd frame is independent.
398
+ const payloadA = compressibleChunk(12_000)
399
+ const payloadB = incompressibleChunk(12_000)
400
+
401
+ const encA = await encodeStreamPayload(payloadA, ZSTD_CODEC)
402
+ const encB = await encodeStreamPayload(payloadB, ZSTD_CODEC)
403
+
404
+ // A is compressible, B is not — distinct encodings.
405
+ assert.equal(encA.encoding, ZSTD_CODEC)
406
+ assert.equal(encB.encoding, undefined) // raw fallback
407
+
408
+ const decA = await decodeStreamPayload(
409
+ {type: "stream.data", encoding: ZSTD_CODEC, dataLength: payloadA.length},
410
+ encA.payload,
411
+ ZSTD_CODEC
412
+ )
413
+ const decB = await decodeStreamPayload(
414
+ {type: "stream.data"},
415
+ encB.payload,
416
+ ZSTD_CODEC
417
+ )
418
+
419
+ assert.deepEqual(decA, payloadA)
420
+ assert.deepEqual(decB, payloadB)
421
+ })