httpcloak 1.5.1 → 1.5.3

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.
package/lib/index.js CHANGED
@@ -235,6 +235,364 @@ class Response {
235
235
  }
236
236
  }
237
237
 
238
+ /**
239
+ * High-performance buffer pool using SharedArrayBuffer for zero-allocation copies.
240
+ * Pre-allocates a large buffer once and reuses it across requests.
241
+ */
242
+ class FastBufferPool {
243
+ constructor() {
244
+ // Pre-allocate 256MB SharedArrayBuffer for maximum performance
245
+ this._sharedBuffer = new SharedArrayBuffer(256 * 1024 * 1024);
246
+ this._bufferView = Buffer.from(this._sharedBuffer);
247
+ this._inUse = false;
248
+
249
+ // Fallback pool for concurrent requests or very large files
250
+ this._fallbackPools = new Map();
251
+ this._tiers = [1024, 4096, 16384, 65536, 262144, 1048576, 4194304, 16777216, 67108864, 134217728];
252
+ }
253
+
254
+ /**
255
+ * Get a buffer of at least the requested size
256
+ * @param {number} size - Minimum buffer size needed
257
+ * @returns {Buffer} - A buffer (may be larger than requested)
258
+ */
259
+ acquire(size) {
260
+ // Use pre-allocated SharedArrayBuffer if available and large enough
261
+ if (!this._inUse && size <= this._sharedBuffer.byteLength) {
262
+ this._inUse = true;
263
+ return this._bufferView;
264
+ }
265
+
266
+ // Fallback to regular buffer pool for concurrent requests
267
+ let tier = this._tiers[this._tiers.length - 1];
268
+ for (const t of this._tiers) {
269
+ if (t >= size) {
270
+ tier = t;
271
+ break;
272
+ }
273
+ }
274
+
275
+ const pool = this._fallbackPools.get(tier);
276
+ if (pool && pool.length > 0) {
277
+ return pool.pop();
278
+ }
279
+
280
+ return Buffer.allocUnsafe(Math.max(tier, size));
281
+ }
282
+
283
+ /**
284
+ * Return a buffer to the pool for reuse
285
+ * @param {Buffer} buffer - Buffer to return
286
+ */
287
+ release(buffer) {
288
+ // Check if this is our shared buffer
289
+ if (buffer.buffer === this._sharedBuffer) {
290
+ this._inUse = false;
291
+ return;
292
+ }
293
+
294
+ // Otherwise add to fallback pool
295
+ const size = buffer.length;
296
+ if (!this._tiers.includes(size)) {
297
+ return;
298
+ }
299
+
300
+ let pool = this._fallbackPools.get(size);
301
+ if (!pool) {
302
+ pool = [];
303
+ this._fallbackPools.set(size, pool);
304
+ }
305
+
306
+ if (pool.length < 2) {
307
+ pool.push(buffer);
308
+ }
309
+ }
310
+ }
311
+
312
+ // Global buffer pool instance
313
+ const _bufferPool = new FastBufferPool();
314
+
315
+ /**
316
+ * Fast response object with zero-copy buffer transfer.
317
+ *
318
+ * This response type avoids JSON serialization and base64 encoding for the body,
319
+ * copying data directly from Go's memory to a Node.js Buffer.
320
+ *
321
+ * Use session.getFast() for maximum download performance.
322
+ */
323
+ class FastResponse {
324
+ /**
325
+ * @param {Object} metadata - Response metadata from native library
326
+ * @param {Buffer} body - Response body as Buffer (view of pooled buffer)
327
+ * @param {number} [elapsed=0] - Elapsed time in milliseconds
328
+ * @param {Buffer} [pooledBuffer=null] - The underlying pooled buffer for release
329
+ */
330
+ constructor(metadata, body, elapsed = 0, pooledBuffer = null) {
331
+ this.statusCode = metadata.status_code || 0;
332
+ this.headers = metadata.headers || {};
333
+ this._body = body;
334
+ this._pooledBuffer = pooledBuffer;
335
+ this.finalUrl = metadata.final_url || "";
336
+ this.protocol = metadata.protocol || "";
337
+ this.elapsed = elapsed;
338
+
339
+ // Parse cookies from response
340
+ this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
341
+
342
+ // Parse redirect history
343
+ this._history = (metadata.history || []).map(h => new RedirectInfo(
344
+ h.status_code || 0,
345
+ h.url || "",
346
+ h.headers || {}
347
+ ));
348
+ }
349
+
350
+ /**
351
+ * Release the underlying buffer back to the pool.
352
+ * Call this when done with the response to enable buffer reuse.
353
+ * After calling release(), the body buffer should not be used.
354
+ */
355
+ release() {
356
+ if (this._pooledBuffer) {
357
+ _bufferPool.release(this._pooledBuffer);
358
+ this._pooledBuffer = null;
359
+ this._body = null;
360
+ }
361
+ }
362
+
363
+ /** Cookies set by this response */
364
+ get cookies() {
365
+ return this._cookies;
366
+ }
367
+
368
+ /** Redirect history (list of RedirectInfo objects) */
369
+ get history() {
370
+ return this._history;
371
+ }
372
+
373
+ /** Response body as string */
374
+ get text() {
375
+ return this._body.toString("utf8");
376
+ }
377
+
378
+ /** Response body as Buffer */
379
+ get body() {
380
+ return this._body;
381
+ }
382
+
383
+ /** Response body as Buffer (requests compatibility alias) */
384
+ get content() {
385
+ return this._body;
386
+ }
387
+
388
+ /** Final URL after redirects (requests compatibility alias) */
389
+ get url() {
390
+ return this.finalUrl;
391
+ }
392
+
393
+ /** True if status code < 400 (requests compatibility) */
394
+ get ok() {
395
+ return this.statusCode < 400;
396
+ }
397
+
398
+ /** HTTP status reason phrase (e.g., 'OK', 'Not Found') */
399
+ get reason() {
400
+ return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
401
+ }
402
+
403
+ /**
404
+ * Response encoding from Content-Type header.
405
+ * Returns null if not specified.
406
+ */
407
+ get encoding() {
408
+ let contentType = this.headers["content-type"] || this.headers["Content-Type"] || "";
409
+ if (contentType.includes("charset=")) {
410
+ const parts = contentType.split(";");
411
+ for (const part of parts) {
412
+ const trimmed = part.trim();
413
+ if (trimmed.toLowerCase().startsWith("charset=")) {
414
+ return trimmed.split("=")[1].trim().replace(/['"]/g, "");
415
+ }
416
+ }
417
+ }
418
+ return null;
419
+ }
420
+
421
+ /**
422
+ * Parse response body as JSON
423
+ */
424
+ json() {
425
+ return JSON.parse(this._body.toString("utf8"));
426
+ }
427
+
428
+ /**
429
+ * Raise error if status >= 400 (requests compatibility)
430
+ */
431
+ raiseForStatus() {
432
+ if (!this.ok) {
433
+ throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
434
+ }
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Streaming HTTP Response for downloading large files.
440
+ *
441
+ * Example:
442
+ * const stream = session.getStream(url);
443
+ * for await (const chunk of stream) {
444
+ * file.write(chunk);
445
+ * }
446
+ * stream.close();
447
+ */
448
+ class StreamResponse {
449
+ /**
450
+ * @param {number} streamHandle - Native stream handle
451
+ * @param {Object} lib - Native library
452
+ * @param {Object} metadata - Stream metadata
453
+ */
454
+ constructor(streamHandle, lib, metadata) {
455
+ this._handle = streamHandle;
456
+ this._lib = lib;
457
+ this.statusCode = metadata.status_code || 0;
458
+ this.headers = metadata.headers || {};
459
+ this.finalUrl = metadata.final_url || "";
460
+ this.protocol = metadata.protocol || "";
461
+ this.contentLength = metadata.content_length || -1;
462
+ this._cookies = (metadata.cookies || []).map(c => new Cookie(c.name || "", c.value || ""));
463
+ this._closed = false;
464
+ }
465
+
466
+ /** Cookies set by this response */
467
+ get cookies() {
468
+ return this._cookies;
469
+ }
470
+
471
+ /** Final URL after redirects */
472
+ get url() {
473
+ return this.finalUrl;
474
+ }
475
+
476
+ /** True if status code < 400 */
477
+ get ok() {
478
+ return this.statusCode < 400;
479
+ }
480
+
481
+ /** HTTP status reason phrase */
482
+ get reason() {
483
+ return HTTP_STATUS_PHRASES[this.statusCode] || "Unknown";
484
+ }
485
+
486
+ /**
487
+ * Read a chunk of data from the stream.
488
+ * @param {number} [chunkSize=8192] - Maximum bytes to read
489
+ * @returns {Buffer|null} - Chunk of data or null if EOF
490
+ */
491
+ readChunk(chunkSize = 8192) {
492
+ if (this._closed) {
493
+ throw new HTTPCloakError("Stream is closed");
494
+ }
495
+
496
+ const result = this._lib.httpcloak_stream_read(this._handle, chunkSize);
497
+ if (!result || result === "") {
498
+ return null; // EOF
499
+ }
500
+
501
+ // Decode base64 to Buffer
502
+ return Buffer.from(result, "base64");
503
+ }
504
+
505
+ /**
506
+ * Async generator for iterating over chunks.
507
+ * @param {number} [chunkSize=8192] - Size of each chunk
508
+ * @yields {Buffer} - Chunks of response content
509
+ *
510
+ * Example:
511
+ * for await (const chunk of stream.iterate()) {
512
+ * file.write(chunk);
513
+ * }
514
+ */
515
+ async *iterate(chunkSize = 8192) {
516
+ while (true) {
517
+ const chunk = this.readChunk(chunkSize);
518
+ if (!chunk) {
519
+ break;
520
+ }
521
+ yield chunk;
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Symbol.asyncIterator for for-await-of loops.
527
+ * @yields {Buffer} - Chunks of response content
528
+ *
529
+ * Example:
530
+ * for await (const chunk of stream) {
531
+ * file.write(chunk);
532
+ * }
533
+ */
534
+ [Symbol.asyncIterator]() {
535
+ return this.iterate();
536
+ }
537
+
538
+ /**
539
+ * Read the entire response body as Buffer.
540
+ * Warning: This defeats the purpose of streaming for large files.
541
+ * @returns {Buffer}
542
+ */
543
+ readAll() {
544
+ const chunks = [];
545
+ let chunk;
546
+ while ((chunk = this.readChunk()) !== null) {
547
+ chunks.push(chunk);
548
+ }
549
+ return Buffer.concat(chunks);
550
+ }
551
+
552
+ /**
553
+ * Read the entire response body as string.
554
+ * @returns {string}
555
+ */
556
+ get text() {
557
+ return this.readAll().toString("utf8");
558
+ }
559
+
560
+ /**
561
+ * Read the entire response body as Buffer.
562
+ * @returns {Buffer}
563
+ */
564
+ get body() {
565
+ return this.readAll();
566
+ }
567
+
568
+ /**
569
+ * Parse the response body as JSON.
570
+ * @returns {any}
571
+ */
572
+ json() {
573
+ return JSON.parse(this.text);
574
+ }
575
+
576
+ /**
577
+ * Close the stream and release resources.
578
+ */
579
+ close() {
580
+ if (!this._closed) {
581
+ this._lib.httpcloak_stream_close(this._handle);
582
+ this._closed = true;
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Raise error if status >= 400
588
+ */
589
+ raiseForStatus() {
590
+ if (!this.ok) {
591
+ throw new HTTPCloakError(`HTTP ${this.statusCode}: ${this.reason}`);
592
+ }
593
+ }
594
+ }
595
+
238
596
  /**
239
597
  * Get the platform package name for the current platform
240
598
  */
@@ -357,6 +715,22 @@ function getLib() {
357
715
  httpcloak_get_async: nativeLibHandle.func("httpcloak_get_async", "void", ["int64", "str", "str", "int64"]),
358
716
  httpcloak_post_async: nativeLibHandle.func("httpcloak_post_async", "void", ["int64", "str", "str", "str", "int64"]),
359
717
  httpcloak_request_async: nativeLibHandle.func("httpcloak_request_async", "void", ["int64", "str", "int64"]),
718
+ // Streaming functions
719
+ httpcloak_stream_get: nativeLibHandle.func("httpcloak_stream_get", "int64", ["int64", "str", "str"]),
720
+ httpcloak_stream_post: nativeLibHandle.func("httpcloak_stream_post", "int64", ["int64", "str", "str", "str"]),
721
+ httpcloak_stream_request: nativeLibHandle.func("httpcloak_stream_request", "int64", ["int64", "str"]),
722
+ httpcloak_stream_get_metadata: nativeLibHandle.func("httpcloak_stream_get_metadata", "str", ["int64"]),
723
+ httpcloak_stream_read: nativeLibHandle.func("httpcloak_stream_read", "str", ["int64", "int64"]),
724
+ httpcloak_stream_close: nativeLibHandle.func("httpcloak_stream_close", "void", ["int64"]),
725
+ // Raw response functions for fast-path (zero-copy)
726
+ httpcloak_get_raw: nativeLibHandle.func("httpcloak_get_raw", "int64", ["int64", "str", "str"]),
727
+ httpcloak_post_raw: nativeLibHandle.func("httpcloak_post_raw", "int64", ["int64", "str", "void*", "int", "str"]),
728
+ httpcloak_response_get_metadata: nativeLibHandle.func("httpcloak_response_get_metadata", "str", ["int64"]),
729
+ httpcloak_response_get_body_len: nativeLibHandle.func("httpcloak_response_get_body_len", "int", ["int64"]),
730
+ httpcloak_response_copy_body_to: nativeLibHandle.func("httpcloak_response_copy_body_to", "int", ["int64", "void*", "int"]),
731
+ httpcloak_response_free: nativeLibHandle.func("httpcloak_response_free", "void", ["int64"]),
732
+ // Combined finalize function (copy + metadata + free in one call)
733
+ httpcloak_response_finalize: nativeLibHandle.func("httpcloak_response_finalize", "str", ["int64", "void*", "int"]),
360
734
  };
361
735
  }
362
736
  return lib;
@@ -674,7 +1048,9 @@ class Session {
674
1048
  * Create a new session
675
1049
  * @param {Object} options - Session options
676
1050
  * @param {string} [options.preset="chrome-143"] - Browser preset to use
677
- * @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port")
1051
+ * @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
1052
+ * @param {string} [options.tcpProxy] - Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config
1053
+ * @param {string} [options.udpProxy] - Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config
678
1054
  * @param {number} [options.timeout=30] - Request timeout in seconds
679
1055
  * @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
680
1056
  * @param {boolean} [options.verify=true] - SSL certificate verification
@@ -683,11 +1059,15 @@ class Session {
683
1059
  * @param {number} [options.retry=3] - Number of retries on failure (set to 0 to disable)
684
1060
  * @param {number[]} [options.retryOnStatus] - Status codes to retry on
685
1061
  * @param {Array} [options.auth] - Default auth [username, password] for all requests
1062
+ * @param {Object} [options.connectTo] - Domain fronting map {requestHost: connectHost}
1063
+ * @param {string} [options.echConfigDomain] - Domain to fetch ECH config from (e.g., "cloudflare-ech.com")
686
1064
  */
687
1065
  constructor(options = {}) {
688
1066
  const {
689
1067
  preset = "chrome-143",
690
1068
  proxy = null,
1069
+ tcpProxy = null,
1070
+ udpProxy = null,
691
1071
  timeout = 30,
692
1072
  httpVersion = "auto",
693
1073
  verify = true,
@@ -697,6 +1077,8 @@ class Session {
697
1077
  retryOnStatus = null,
698
1078
  preferIpv4 = false,
699
1079
  auth = null,
1080
+ connectTo = null,
1081
+ echConfigDomain = null,
700
1082
  } = options;
701
1083
 
702
1084
  this._lib = getLib();
@@ -711,6 +1093,12 @@ class Session {
711
1093
  if (proxy) {
712
1094
  config.proxy = proxy;
713
1095
  }
1096
+ if (tcpProxy) {
1097
+ config.tcp_proxy = tcpProxy;
1098
+ }
1099
+ if (udpProxy) {
1100
+ config.udp_proxy = udpProxy;
1101
+ }
714
1102
  if (!verify) {
715
1103
  config.verify = false;
716
1104
  }
@@ -727,6 +1115,12 @@ class Session {
727
1115
  if (preferIpv4) {
728
1116
  config.prefer_ipv4 = true;
729
1117
  }
1118
+ if (connectTo) {
1119
+ config.connect_to = connectTo;
1120
+ }
1121
+ if (echConfigDomain) {
1122
+ config.ech_config_domain = echConfigDomain;
1123
+ }
730
1124
 
731
1125
  this._handle = this._lib.httpcloak_session_new(JSON.stringify(config));
732
1126
 
@@ -804,9 +1198,15 @@ class Session {
804
1198
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
805
1199
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
806
1200
 
807
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1201
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1202
+ const reqOptions = {};
1203
+ if (mergedHeaders) {
1204
+ reqOptions.headers = mergedHeaders;
1205
+ }
1206
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1207
+
808
1208
  const startTime = Date.now();
809
- const result = this._lib.httpcloak_get(this._handle, url, headersJson);
1209
+ const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
810
1210
  const elapsed = Date.now() - startTime;
811
1211
  return parseResponse(result, elapsed);
812
1212
  }
@@ -868,9 +1268,15 @@ class Session {
868
1268
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
869
1269
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
870
1270
 
871
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1271
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1272
+ const reqOptions = {};
1273
+ if (mergedHeaders) {
1274
+ reqOptions.headers = mergedHeaders;
1275
+ }
1276
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1277
+
872
1278
  const startTime = Date.now();
873
- const result = this._lib.httpcloak_post(this._handle, url, body, headersJson);
1279
+ const result = this._lib.httpcloak_post(this._handle, url, body, optionsJson);
874
1280
  const elapsed = Date.now() - startTime;
875
1281
  return parseResponse(result, elapsed);
876
1282
  }
@@ -962,14 +1368,19 @@ class Session {
962
1368
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
963
1369
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
964
1370
 
965
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1371
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1372
+ const reqOptions = {};
1373
+ if (mergedHeaders) {
1374
+ reqOptions.headers = mergedHeaders;
1375
+ }
1376
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
966
1377
 
967
1378
  // Register async request with callback manager
968
1379
  const manager = getAsyncManager();
969
1380
  const { callbackId, promise } = manager.registerRequest(this._lib);
970
1381
 
971
1382
  // Start async request
972
- this._lib.httpcloak_get_async(this._handle, url, headersJson, callbackId);
1383
+ this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
973
1384
 
974
1385
  return promise;
975
1386
  }
@@ -1021,14 +1432,19 @@ class Session {
1021
1432
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1022
1433
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1023
1434
 
1024
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1435
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1436
+ const reqOptions = {};
1437
+ if (mergedHeaders) {
1438
+ reqOptions.headers = mergedHeaders;
1439
+ }
1440
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1025
1441
 
1026
1442
  // Register async request with callback manager
1027
1443
  const manager = getAsyncManager();
1028
1444
  const { callbackId, promise } = manager.registerRequest(this._lib);
1029
1445
 
1030
1446
  // Start async request
1031
- this._lib.httpcloak_post_async(this._handle, url, body, headersJson, callbackId);
1447
+ this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
1032
1448
 
1033
1449
  return promise;
1034
1450
  }
@@ -1195,6 +1611,394 @@ class Session {
1195
1611
  get cookies() {
1196
1612
  return this.getCookies();
1197
1613
  }
1614
+
1615
+ // ===========================================================================
1616
+ // Streaming Methods
1617
+ // ===========================================================================
1618
+
1619
+ /**
1620
+ * Perform a streaming GET request.
1621
+ *
1622
+ * @param {string} url - Request URL
1623
+ * @param {Object} [options] - Request options
1624
+ * @param {Object} [options.params] - URL query parameters
1625
+ * @param {Object} [options.headers] - Request headers
1626
+ * @param {Object} [options.cookies] - Cookies to send
1627
+ * @param {number} [options.timeout] - Request timeout in milliseconds
1628
+ * @returns {StreamResponse} - Streaming response for chunked reading
1629
+ *
1630
+ * Example:
1631
+ * const stream = session.getStream("https://example.com/large-file.zip");
1632
+ * for await (const chunk of stream) {
1633
+ * file.write(chunk);
1634
+ * }
1635
+ * stream.close();
1636
+ */
1637
+ getStream(url, options = {}) {
1638
+ const { params, headers, cookies, timeout } = options;
1639
+
1640
+ // Add params to URL
1641
+ if (params) {
1642
+ url = addParamsToUrl(url, params);
1643
+ }
1644
+
1645
+ // Merge headers
1646
+ let mergedHeaders = { ...this.headers };
1647
+ if (headers) {
1648
+ mergedHeaders = { ...mergedHeaders, ...headers };
1649
+ }
1650
+ if (cookies) {
1651
+ const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
1652
+ mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
1653
+ ? `${mergedHeaders["Cookie"]}; ${cookieStr}`
1654
+ : cookieStr;
1655
+ }
1656
+
1657
+ // Build options JSON
1658
+ const reqOptions = {};
1659
+ if (Object.keys(mergedHeaders).length > 0) {
1660
+ reqOptions.headers = mergedHeaders;
1661
+ }
1662
+ if (timeout) {
1663
+ reqOptions.timeout = timeout;
1664
+ }
1665
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1666
+
1667
+ // Start stream
1668
+ const streamHandle = this._lib.httpcloak_stream_get(this._handle, url, optionsJson);
1669
+ if (streamHandle < 0) {
1670
+ throw new HTTPCloakError("Failed to start streaming request");
1671
+ }
1672
+
1673
+ // Get metadata
1674
+ const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
1675
+ if (!metadataStr) {
1676
+ this._lib.httpcloak_stream_close(streamHandle);
1677
+ throw new HTTPCloakError("Failed to get stream metadata");
1678
+ }
1679
+
1680
+ const metadata = JSON.parse(metadataStr);
1681
+ if (metadata.error) {
1682
+ this._lib.httpcloak_stream_close(streamHandle);
1683
+ throw new HTTPCloakError(metadata.error);
1684
+ }
1685
+
1686
+ return new StreamResponse(streamHandle, this._lib, metadata);
1687
+ }
1688
+
1689
+ /**
1690
+ * Perform a streaming POST request.
1691
+ *
1692
+ * @param {string} url - Request URL
1693
+ * @param {Object} [options] - Request options
1694
+ * @param {string|Buffer|Object} [options.body] - Request body
1695
+ * @param {Object} [options.json] - JSON body (will be serialized)
1696
+ * @param {Object} [options.form] - Form data (will be URL-encoded)
1697
+ * @param {Object} [options.params] - URL query parameters
1698
+ * @param {Object} [options.headers] - Request headers
1699
+ * @param {Object} [options.cookies] - Cookies to send
1700
+ * @param {number} [options.timeout] - Request timeout in milliseconds
1701
+ * @returns {StreamResponse} - Streaming response for chunked reading
1702
+ */
1703
+ postStream(url, options = {}) {
1704
+ const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
1705
+
1706
+ // Add params to URL
1707
+ if (params) {
1708
+ url = addParamsToUrl(url, params);
1709
+ }
1710
+
1711
+ // Merge headers
1712
+ let mergedHeaders = { ...this.headers };
1713
+ if (headers) {
1714
+ mergedHeaders = { ...mergedHeaders, ...headers };
1715
+ }
1716
+
1717
+ // Process body
1718
+ let body = null;
1719
+ if (jsonBody) {
1720
+ body = JSON.stringify(jsonBody);
1721
+ mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/json";
1722
+ } else if (form) {
1723
+ body = Object.entries(form).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1724
+ mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/x-www-form-urlencoded";
1725
+ } else if (bodyOpt) {
1726
+ body = typeof bodyOpt === "string" ? bodyOpt : bodyOpt.toString();
1727
+ }
1728
+
1729
+ if (cookies) {
1730
+ const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
1731
+ mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
1732
+ ? `${mergedHeaders["Cookie"]}; ${cookieStr}`
1733
+ : cookieStr;
1734
+ }
1735
+
1736
+ // Build options JSON
1737
+ const reqOptions = {};
1738
+ if (Object.keys(mergedHeaders).length > 0) {
1739
+ reqOptions.headers = mergedHeaders;
1740
+ }
1741
+ if (timeout) {
1742
+ reqOptions.timeout = timeout;
1743
+ }
1744
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1745
+
1746
+ // Start stream
1747
+ const streamHandle = this._lib.httpcloak_stream_post(this._handle, url, body, optionsJson);
1748
+ if (streamHandle < 0) {
1749
+ throw new HTTPCloakError("Failed to start streaming request");
1750
+ }
1751
+
1752
+ // Get metadata
1753
+ const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
1754
+ if (!metadataStr) {
1755
+ this._lib.httpcloak_stream_close(streamHandle);
1756
+ throw new HTTPCloakError("Failed to get stream metadata");
1757
+ }
1758
+
1759
+ const metadata = JSON.parse(metadataStr);
1760
+ if (metadata.error) {
1761
+ this._lib.httpcloak_stream_close(streamHandle);
1762
+ throw new HTTPCloakError(metadata.error);
1763
+ }
1764
+
1765
+ return new StreamResponse(streamHandle, this._lib, metadata);
1766
+ }
1767
+
1768
+ /**
1769
+ * Perform a streaming request with any HTTP method.
1770
+ *
1771
+ * @param {string} method - HTTP method
1772
+ * @param {string} url - Request URL
1773
+ * @param {Object} [options] - Request options
1774
+ * @param {string|Buffer} [options.body] - Request body
1775
+ * @param {Object} [options.params] - URL query parameters
1776
+ * @param {Object} [options.headers] - Request headers
1777
+ * @param {Object} [options.cookies] - Cookies to send
1778
+ * @param {number} [options.timeout] - Request timeout in seconds
1779
+ * @returns {StreamResponse} - Streaming response for chunked reading
1780
+ */
1781
+ requestStream(method, url, options = {}) {
1782
+ const { body, params, headers, cookies, timeout } = options;
1783
+
1784
+ // Add params to URL
1785
+ if (params) {
1786
+ url = addParamsToUrl(url, params);
1787
+ }
1788
+
1789
+ // Merge headers
1790
+ let mergedHeaders = { ...this.headers };
1791
+ if (headers) {
1792
+ mergedHeaders = { ...mergedHeaders, ...headers };
1793
+ }
1794
+ if (cookies) {
1795
+ const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
1796
+ mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
1797
+ ? `${mergedHeaders["Cookie"]}; ${cookieStr}`
1798
+ : cookieStr;
1799
+ }
1800
+
1801
+ // Build request config
1802
+ const requestConfig = {
1803
+ method: method.toUpperCase(),
1804
+ url,
1805
+ };
1806
+ if (Object.keys(mergedHeaders).length > 0) {
1807
+ requestConfig.headers = mergedHeaders;
1808
+ }
1809
+ if (body) {
1810
+ requestConfig.body = typeof body === "string" ? body : body.toString();
1811
+ }
1812
+ if (timeout) {
1813
+ requestConfig.timeout = timeout;
1814
+ }
1815
+
1816
+ // Start stream
1817
+ const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
1818
+ if (streamHandle < 0) {
1819
+ throw new HTTPCloakError("Failed to start streaming request");
1820
+ }
1821
+
1822
+ // Get metadata
1823
+ const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
1824
+ if (!metadataStr) {
1825
+ this._lib.httpcloak_stream_close(streamHandle);
1826
+ throw new HTTPCloakError("Failed to get stream metadata");
1827
+ }
1828
+
1829
+ const metadata = JSON.parse(metadataStr);
1830
+ if (metadata.error) {
1831
+ this._lib.httpcloak_stream_close(streamHandle);
1832
+ throw new HTTPCloakError(metadata.error);
1833
+ }
1834
+
1835
+ return new StreamResponse(streamHandle, this._lib, metadata);
1836
+ }
1837
+
1838
+ // ===========================================================================
1839
+ // Fast-path Methods (Zero-copy for maximum performance)
1840
+ // ===========================================================================
1841
+
1842
+ /**
1843
+ * Perform a fast GET request with zero-copy buffer transfer.
1844
+ *
1845
+ * This method bypasses JSON serialization and base64 encoding for the response body,
1846
+ * copying data directly from Go's memory to a Node.js Buffer.
1847
+ *
1848
+ * Use this method for downloading large files when you need maximum throughput.
1849
+ *
1850
+ * @param {string} url - Request URL
1851
+ * @param {Object} [options] - Request options
1852
+ * @param {Object} [options.headers] - Custom headers
1853
+ * @param {Object} [options.params] - Query parameters
1854
+ * @param {Object} [options.cookies] - Cookies to send with this request
1855
+ * @param {Array} [options.auth] - Basic auth [username, password]
1856
+ * @returns {FastResponse} Fast response object with Buffer body
1857
+ *
1858
+ * Example:
1859
+ * const response = session.getFast("https://example.com/large-file.zip");
1860
+ * console.log(`Downloaded ${response.body.length} bytes`);
1861
+ * fs.writeFileSync("file.zip", response.body);
1862
+ */
1863
+ getFast(url, options = {}) {
1864
+ const { headers = null, params = null, cookies = null, auth = null } = options;
1865
+
1866
+ url = addParamsToUrl(url, params);
1867
+ let mergedHeaders = this._mergeHeaders(headers);
1868
+ // Use request auth if provided, otherwise fall back to session auth
1869
+ const effectiveAuth = auth !== null ? auth : this.auth;
1870
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1871
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1872
+
1873
+ // Build request options JSON with headers wrapper
1874
+ const reqOptions = {};
1875
+ if (mergedHeaders) {
1876
+ reqOptions.headers = mergedHeaders;
1877
+ }
1878
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1879
+
1880
+ const startTime = Date.now();
1881
+
1882
+ // Get raw response handle
1883
+ const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
1884
+ if (responseHandle === 0 || responseHandle === 0n) {
1885
+ throw new HTTPCloakError("Failed to make request");
1886
+ }
1887
+
1888
+ // Get body length first (1 FFI call)
1889
+ const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
1890
+ if (bodyLen < 0) {
1891
+ this._lib.httpcloak_response_free(responseHandle);
1892
+ throw new HTTPCloakError("Failed to get response body length");
1893
+ }
1894
+
1895
+ // Acquire pooled buffer
1896
+ const pooledBuffer = _bufferPool.acquire(bodyLen);
1897
+
1898
+ // Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
1899
+ const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
1900
+ if (!metadataStr) {
1901
+ _bufferPool.release(pooledBuffer);
1902
+ throw new HTTPCloakError("Failed to finalize response");
1903
+ }
1904
+
1905
+ const metadata = JSON.parse(metadataStr);
1906
+ if (metadata.error) {
1907
+ _bufferPool.release(pooledBuffer);
1908
+ throw new HTTPCloakError(metadata.error);
1909
+ }
1910
+
1911
+ // Create a view of just the used portion
1912
+ const buffer = pooledBuffer.subarray(0, bodyLen);
1913
+
1914
+ const elapsed = Date.now() - startTime;
1915
+ return new FastResponse(metadata, buffer, elapsed, pooledBuffer);
1916
+ }
1917
+
1918
+ /**
1919
+ * High-performance POST request optimized for large uploads.
1920
+ *
1921
+ * Uses binary buffer passing and response pooling for maximum throughput.
1922
+ * Call response.release() when done to return buffers to pool.
1923
+ *
1924
+ * @param {string} url - Request URL
1925
+ * @param {Object} [options] - Request options
1926
+ * @param {Buffer} [options.body] - Request body as Buffer
1927
+ * @param {Object} [options.headers] - Request headers
1928
+ * @param {Object} [options.params] - Query parameters
1929
+ * @param {Object} [options.cookies] - Cookies to send with this request
1930
+ * @param {Array} [options.auth] - Basic auth [username, password]
1931
+ * @returns {FastResponse} Fast response object with Buffer body
1932
+ *
1933
+ * Example:
1934
+ * const data = Buffer.alloc(10 * 1024 * 1024); // 10MB
1935
+ * const response = session.postFast("https://example.com/upload", { body: data });
1936
+ * console.log(`Uploaded, response: ${response.statusCode}`);
1937
+ * response.release();
1938
+ */
1939
+ postFast(url, options = {}) {
1940
+ let { body = null, headers = null, params = null, cookies = null, auth = null } = options;
1941
+
1942
+ url = addParamsToUrl(url, params);
1943
+ let mergedHeaders = this._mergeHeaders(headers);
1944
+ // Use request auth if provided, otherwise fall back to session auth
1945
+ const effectiveAuth = auth !== null ? auth : this.auth;
1946
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1947
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1948
+
1949
+ // Ensure body is a Buffer
1950
+ if (body === null) {
1951
+ body = Buffer.alloc(0);
1952
+ } else if (typeof body === "string") {
1953
+ body = Buffer.from(body, "utf8");
1954
+ } else if (!Buffer.isBuffer(body)) {
1955
+ throw new HTTPCloakError("postFast body must be a Buffer or string");
1956
+ }
1957
+
1958
+ // Build request options JSON with headers wrapper
1959
+ const reqOptions = {};
1960
+ if (mergedHeaders) {
1961
+ reqOptions.headers = mergedHeaders;
1962
+ }
1963
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1964
+
1965
+ const startTime = Date.now();
1966
+
1967
+ // Use httpcloak_post_raw with binary buffer (no string conversion!)
1968
+ const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, body, body.length, optionsJson);
1969
+ if (responseHandle === 0 || responseHandle === 0n || responseHandle < 0) {
1970
+ throw new HTTPCloakError("Failed to make POST request");
1971
+ }
1972
+
1973
+ // Get body length first (1 FFI call)
1974
+ const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
1975
+ if (bodyLen < 0) {
1976
+ this._lib.httpcloak_response_free(responseHandle);
1977
+ throw new HTTPCloakError("Failed to get response body length");
1978
+ }
1979
+
1980
+ // Acquire pooled buffer for response
1981
+ const pooledBuffer = _bufferPool.acquire(bodyLen);
1982
+
1983
+ // Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
1984
+ const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
1985
+ if (!metadataStr) {
1986
+ _bufferPool.release(pooledBuffer);
1987
+ throw new HTTPCloakError("Failed to finalize response");
1988
+ }
1989
+
1990
+ const metadata = JSON.parse(metadataStr);
1991
+ if (metadata.error) {
1992
+ _bufferPool.release(pooledBuffer);
1993
+ throw new HTTPCloakError(metadata.error);
1994
+ }
1995
+
1996
+ // Create a view of just the used portion
1997
+ const responseBuffer = pooledBuffer.subarray(0, bodyLen);
1998
+
1999
+ const elapsed = Date.now() - startTime;
2000
+ return new FastResponse(metadata, responseBuffer, elapsed, pooledBuffer);
2001
+ }
1198
2002
  }
1199
2003
 
1200
2004
  // =============================================================================
@@ -1374,6 +2178,8 @@ module.exports = {
1374
2178
  // Classes
1375
2179
  Session,
1376
2180
  Response,
2181
+ FastResponse,
2182
+ StreamResponse,
1377
2183
  Cookie,
1378
2184
  RedirectInfo,
1379
2185
  HTTPCloakError,