httpcloak 1.5.2 → 1.5.4

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,27 @@ 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"]),
734
+ // Session persistence functions
735
+ httpcloak_session_save: nativeLibHandle.func("httpcloak_session_save", "str", ["int64", "str"]),
736
+ httpcloak_session_load: nativeLibHandle.func("httpcloak_session_load", "int64", ["str"]),
737
+ httpcloak_session_marshal: nativeLibHandle.func("httpcloak_session_marshal", "str", ["int64"]),
738
+ httpcloak_session_unmarshal: nativeLibHandle.func("httpcloak_session_unmarshal", "int64", ["str"]),
360
739
  };
361
740
  }
362
741
  return lib;
@@ -675,6 +1054,8 @@ class Session {
675
1054
  * @param {Object} options - Session options
676
1055
  * @param {string} [options.preset="chrome-143"] - Browser preset to use
677
1056
  * @param {string} [options.proxy] - Proxy URL (e.g., "http://user:pass@host:port" or "socks5://host:port")
1057
+ * @param {string} [options.tcpProxy] - Proxy URL for TCP protocols (HTTP/1.1, HTTP/2) - use with udpProxy for split config
1058
+ * @param {string} [options.udpProxy] - Proxy URL for UDP protocols (HTTP/3 via MASQUE) - use with tcpProxy for split config
678
1059
  * @param {number} [options.timeout=30] - Request timeout in seconds
679
1060
  * @param {string} [options.httpVersion="auto"] - HTTP version: "auto", "h1", "h2", "h3"
680
1061
  * @param {boolean} [options.verify=true] - SSL certificate verification
@@ -690,6 +1071,8 @@ class Session {
690
1071
  const {
691
1072
  preset = "chrome-143",
692
1073
  proxy = null,
1074
+ tcpProxy = null,
1075
+ udpProxy = null,
693
1076
  timeout = 30,
694
1077
  httpVersion = "auto",
695
1078
  verify = true,
@@ -715,6 +1098,12 @@ class Session {
715
1098
  if (proxy) {
716
1099
  config.proxy = proxy;
717
1100
  }
1101
+ if (tcpProxy) {
1102
+ config.tcp_proxy = tcpProxy;
1103
+ }
1104
+ if (udpProxy) {
1105
+ config.udp_proxy = udpProxy;
1106
+ }
718
1107
  if (!verify) {
719
1108
  config.verify = false;
720
1109
  }
@@ -814,9 +1203,15 @@ class Session {
814
1203
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
815
1204
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
816
1205
 
817
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1206
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1207
+ const reqOptions = {};
1208
+ if (mergedHeaders) {
1209
+ reqOptions.headers = mergedHeaders;
1210
+ }
1211
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1212
+
818
1213
  const startTime = Date.now();
819
- const result = this._lib.httpcloak_get(this._handle, url, headersJson);
1214
+ const result = this._lib.httpcloak_get(this._handle, url, optionsJson);
820
1215
  const elapsed = Date.now() - startTime;
821
1216
  return parseResponse(result, elapsed);
822
1217
  }
@@ -878,9 +1273,15 @@ class Session {
878
1273
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
879
1274
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
880
1275
 
881
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1276
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1277
+ const reqOptions = {};
1278
+ if (mergedHeaders) {
1279
+ reqOptions.headers = mergedHeaders;
1280
+ }
1281
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1282
+
882
1283
  const startTime = Date.now();
883
- const result = this._lib.httpcloak_post(this._handle, url, body, headersJson);
1284
+ const result = this._lib.httpcloak_post(this._handle, url, body, optionsJson);
884
1285
  const elapsed = Date.now() - startTime;
885
1286
  return parseResponse(result, elapsed);
886
1287
  }
@@ -972,14 +1373,19 @@ class Session {
972
1373
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
973
1374
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
974
1375
 
975
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1376
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1377
+ const reqOptions = {};
1378
+ if (mergedHeaders) {
1379
+ reqOptions.headers = mergedHeaders;
1380
+ }
1381
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
976
1382
 
977
1383
  // Register async request with callback manager
978
1384
  const manager = getAsyncManager();
979
1385
  const { callbackId, promise } = manager.registerRequest(this._lib);
980
1386
 
981
1387
  // Start async request
982
- this._lib.httpcloak_get_async(this._handle, url, headersJson, callbackId);
1388
+ this._lib.httpcloak_get_async(this._handle, url, optionsJson, callbackId);
983
1389
 
984
1390
  return promise;
985
1391
  }
@@ -1031,14 +1437,19 @@ class Session {
1031
1437
  mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1032
1438
  mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1033
1439
 
1034
- const headersJson = mergedHeaders ? JSON.stringify(mergedHeaders) : null;
1440
+ // Build request options JSON with headers wrapper (clib expects {"headers": {...}})
1441
+ const reqOptions = {};
1442
+ if (mergedHeaders) {
1443
+ reqOptions.headers = mergedHeaders;
1444
+ }
1445
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1035
1446
 
1036
1447
  // Register async request with callback manager
1037
1448
  const manager = getAsyncManager();
1038
1449
  const { callbackId, promise } = manager.registerRequest(this._lib);
1039
1450
 
1040
1451
  // Start async request
1041
- this._lib.httpcloak_post_async(this._handle, url, body, headersJson, callbackId);
1452
+ this._lib.httpcloak_post_async(this._handle, url, body, optionsJson, callbackId);
1042
1453
 
1043
1454
  return promise;
1044
1455
  }
@@ -1205,6 +1616,515 @@ class Session {
1205
1616
  get cookies() {
1206
1617
  return this.getCookies();
1207
1618
  }
1619
+
1620
+ // ===========================================================================
1621
+ // Session Persistence
1622
+ // ===========================================================================
1623
+
1624
+ /**
1625
+ * Save session state (cookies, TLS sessions) to a file.
1626
+ *
1627
+ * This allows you to persist session state across program runs,
1628
+ * including cookies and TLS session tickets for faster resumption.
1629
+ *
1630
+ * @param {string} path - Path to save the session file
1631
+ *
1632
+ * Example:
1633
+ * const session = new httpcloak.Session({ preset: "chrome-143" });
1634
+ * await session.get("https://example.com"); // Acquire cookies
1635
+ * session.save("session.json");
1636
+ *
1637
+ * // Later, restore the session
1638
+ * const session = httpcloak.Session.load("session.json");
1639
+ */
1640
+ save(path) {
1641
+ const result = this._lib.httpcloak_session_save(this._handle, path);
1642
+ if (result) {
1643
+ const data = JSON.parse(result);
1644
+ if (data.error) {
1645
+ throw new HTTPCloakError(data.error);
1646
+ }
1647
+ }
1648
+ }
1649
+
1650
+ /**
1651
+ * Export session state to JSON string.
1652
+ *
1653
+ * @returns {string} JSON string containing session state
1654
+ *
1655
+ * Example:
1656
+ * const sessionData = session.marshal();
1657
+ * // Store sessionData in database, cache, etc.
1658
+ *
1659
+ * // Later, restore the session
1660
+ * const session = httpcloak.Session.unmarshal(sessionData);
1661
+ */
1662
+ marshal() {
1663
+ const result = this._lib.httpcloak_session_marshal(this._handle);
1664
+ if (!result) {
1665
+ throw new HTTPCloakError("Failed to marshal session");
1666
+ }
1667
+
1668
+ // Check for error
1669
+ try {
1670
+ const data = JSON.parse(result);
1671
+ if (data && typeof data === "object" && data.error) {
1672
+ throw new HTTPCloakError(data.error);
1673
+ }
1674
+ } catch (e) {
1675
+ if (e instanceof HTTPCloakError) throw e;
1676
+ // Not an error response, just JSON parse failed - return as is
1677
+ }
1678
+
1679
+ return result;
1680
+ }
1681
+
1682
+ /**
1683
+ * Load a session from a file.
1684
+ *
1685
+ * This restores session state including cookies and TLS session tickets.
1686
+ * The session uses the same preset that was used when it was saved.
1687
+ *
1688
+ * @param {string} path - Path to the session file
1689
+ * @returns {Session} Restored Session object
1690
+ *
1691
+ * Example:
1692
+ * const session = httpcloak.Session.load("session.json");
1693
+ * const r = await session.get("https://example.com"); // Uses restored cookies
1694
+ */
1695
+ static load(path) {
1696
+ const lib = getLib();
1697
+ const handle = lib.httpcloak_session_load(path);
1698
+
1699
+ if (handle < 0 || handle === 0n) {
1700
+ throw new HTTPCloakError(`Failed to load session from ${path}`);
1701
+ }
1702
+
1703
+ // Create a new Session instance without calling constructor
1704
+ const session = Object.create(Session.prototype);
1705
+ session._lib = lib;
1706
+ session._handle = handle;
1707
+ session.headers = {};
1708
+ session.auth = null;
1709
+
1710
+ return session;
1711
+ }
1712
+
1713
+ /**
1714
+ * Load a session from JSON string.
1715
+ *
1716
+ * @param {string} data - JSON string containing session state
1717
+ * @returns {Session} Restored Session object
1718
+ *
1719
+ * Example:
1720
+ * // Retrieve sessionData from database, cache, etc.
1721
+ * const session = httpcloak.Session.unmarshal(sessionData);
1722
+ */
1723
+ static unmarshal(data) {
1724
+ const lib = getLib();
1725
+ const handle = lib.httpcloak_session_unmarshal(data);
1726
+
1727
+ if (handle < 0 || handle === 0n) {
1728
+ throw new HTTPCloakError("Failed to unmarshal session");
1729
+ }
1730
+
1731
+ // Create a new Session instance without calling constructor
1732
+ const session = Object.create(Session.prototype);
1733
+ session._lib = lib;
1734
+ session._handle = handle;
1735
+ session.headers = {};
1736
+ session.auth = null;
1737
+
1738
+ return session;
1739
+ }
1740
+
1741
+ // ===========================================================================
1742
+ // Streaming Methods
1743
+ // ===========================================================================
1744
+
1745
+ /**
1746
+ * Perform a streaming GET request.
1747
+ *
1748
+ * @param {string} url - Request URL
1749
+ * @param {Object} [options] - Request options
1750
+ * @param {Object} [options.params] - URL query parameters
1751
+ * @param {Object} [options.headers] - Request headers
1752
+ * @param {Object} [options.cookies] - Cookies to send
1753
+ * @param {number} [options.timeout] - Request timeout in milliseconds
1754
+ * @returns {StreamResponse} - Streaming response for chunked reading
1755
+ *
1756
+ * Example:
1757
+ * const stream = session.getStream("https://example.com/large-file.zip");
1758
+ * for await (const chunk of stream) {
1759
+ * file.write(chunk);
1760
+ * }
1761
+ * stream.close();
1762
+ */
1763
+ getStream(url, options = {}) {
1764
+ const { params, headers, cookies, timeout } = options;
1765
+
1766
+ // Add params to URL
1767
+ if (params) {
1768
+ url = addParamsToUrl(url, params);
1769
+ }
1770
+
1771
+ // Merge headers
1772
+ let mergedHeaders = { ...this.headers };
1773
+ if (headers) {
1774
+ mergedHeaders = { ...mergedHeaders, ...headers };
1775
+ }
1776
+ if (cookies) {
1777
+ const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
1778
+ mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
1779
+ ? `${mergedHeaders["Cookie"]}; ${cookieStr}`
1780
+ : cookieStr;
1781
+ }
1782
+
1783
+ // Build options JSON
1784
+ const reqOptions = {};
1785
+ if (Object.keys(mergedHeaders).length > 0) {
1786
+ reqOptions.headers = mergedHeaders;
1787
+ }
1788
+ if (timeout) {
1789
+ reqOptions.timeout = timeout;
1790
+ }
1791
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1792
+
1793
+ // Start stream
1794
+ const streamHandle = this._lib.httpcloak_stream_get(this._handle, url, optionsJson);
1795
+ if (streamHandle < 0) {
1796
+ throw new HTTPCloakError("Failed to start streaming request");
1797
+ }
1798
+
1799
+ // Get metadata
1800
+ const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
1801
+ if (!metadataStr) {
1802
+ this._lib.httpcloak_stream_close(streamHandle);
1803
+ throw new HTTPCloakError("Failed to get stream metadata");
1804
+ }
1805
+
1806
+ const metadata = JSON.parse(metadataStr);
1807
+ if (metadata.error) {
1808
+ this._lib.httpcloak_stream_close(streamHandle);
1809
+ throw new HTTPCloakError(metadata.error);
1810
+ }
1811
+
1812
+ return new StreamResponse(streamHandle, this._lib, metadata);
1813
+ }
1814
+
1815
+ /**
1816
+ * Perform a streaming POST request.
1817
+ *
1818
+ * @param {string} url - Request URL
1819
+ * @param {Object} [options] - Request options
1820
+ * @param {string|Buffer|Object} [options.body] - Request body
1821
+ * @param {Object} [options.json] - JSON body (will be serialized)
1822
+ * @param {Object} [options.form] - Form data (will be URL-encoded)
1823
+ * @param {Object} [options.params] - URL query parameters
1824
+ * @param {Object} [options.headers] - Request headers
1825
+ * @param {Object} [options.cookies] - Cookies to send
1826
+ * @param {number} [options.timeout] - Request timeout in milliseconds
1827
+ * @returns {StreamResponse} - Streaming response for chunked reading
1828
+ */
1829
+ postStream(url, options = {}) {
1830
+ const { body: bodyOpt, json: jsonBody, form, params, headers, cookies, timeout } = options;
1831
+
1832
+ // Add params to URL
1833
+ if (params) {
1834
+ url = addParamsToUrl(url, params);
1835
+ }
1836
+
1837
+ // Merge headers
1838
+ let mergedHeaders = { ...this.headers };
1839
+ if (headers) {
1840
+ mergedHeaders = { ...mergedHeaders, ...headers };
1841
+ }
1842
+
1843
+ // Process body
1844
+ let body = null;
1845
+ if (jsonBody) {
1846
+ body = JSON.stringify(jsonBody);
1847
+ mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/json";
1848
+ } else if (form) {
1849
+ body = Object.entries(form).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
1850
+ mergedHeaders["Content-Type"] = mergedHeaders["Content-Type"] || "application/x-www-form-urlencoded";
1851
+ } else if (bodyOpt) {
1852
+ body = typeof bodyOpt === "string" ? bodyOpt : bodyOpt.toString();
1853
+ }
1854
+
1855
+ if (cookies) {
1856
+ const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
1857
+ mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
1858
+ ? `${mergedHeaders["Cookie"]}; ${cookieStr}`
1859
+ : cookieStr;
1860
+ }
1861
+
1862
+ // Build options JSON
1863
+ const reqOptions = {};
1864
+ if (Object.keys(mergedHeaders).length > 0) {
1865
+ reqOptions.headers = mergedHeaders;
1866
+ }
1867
+ if (timeout) {
1868
+ reqOptions.timeout = timeout;
1869
+ }
1870
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
1871
+
1872
+ // Start stream
1873
+ const streamHandle = this._lib.httpcloak_stream_post(this._handle, url, body, optionsJson);
1874
+ if (streamHandle < 0) {
1875
+ throw new HTTPCloakError("Failed to start streaming request");
1876
+ }
1877
+
1878
+ // Get metadata
1879
+ const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
1880
+ if (!metadataStr) {
1881
+ this._lib.httpcloak_stream_close(streamHandle);
1882
+ throw new HTTPCloakError("Failed to get stream metadata");
1883
+ }
1884
+
1885
+ const metadata = JSON.parse(metadataStr);
1886
+ if (metadata.error) {
1887
+ this._lib.httpcloak_stream_close(streamHandle);
1888
+ throw new HTTPCloakError(metadata.error);
1889
+ }
1890
+
1891
+ return new StreamResponse(streamHandle, this._lib, metadata);
1892
+ }
1893
+
1894
+ /**
1895
+ * Perform a streaming request with any HTTP method.
1896
+ *
1897
+ * @param {string} method - HTTP method
1898
+ * @param {string} url - Request URL
1899
+ * @param {Object} [options] - Request options
1900
+ * @param {string|Buffer} [options.body] - Request body
1901
+ * @param {Object} [options.params] - URL query parameters
1902
+ * @param {Object} [options.headers] - Request headers
1903
+ * @param {Object} [options.cookies] - Cookies to send
1904
+ * @param {number} [options.timeout] - Request timeout in seconds
1905
+ * @returns {StreamResponse} - Streaming response for chunked reading
1906
+ */
1907
+ requestStream(method, url, options = {}) {
1908
+ const { body, params, headers, cookies, timeout } = options;
1909
+
1910
+ // Add params to URL
1911
+ if (params) {
1912
+ url = addParamsToUrl(url, params);
1913
+ }
1914
+
1915
+ // Merge headers
1916
+ let mergedHeaders = { ...this.headers };
1917
+ if (headers) {
1918
+ mergedHeaders = { ...mergedHeaders, ...headers };
1919
+ }
1920
+ if (cookies) {
1921
+ const cookieStr = Object.entries(cookies).map(([k, v]) => `${k}=${v}`).join("; ");
1922
+ mergedHeaders["Cookie"] = mergedHeaders["Cookie"]
1923
+ ? `${mergedHeaders["Cookie"]}; ${cookieStr}`
1924
+ : cookieStr;
1925
+ }
1926
+
1927
+ // Build request config
1928
+ const requestConfig = {
1929
+ method: method.toUpperCase(),
1930
+ url,
1931
+ };
1932
+ if (Object.keys(mergedHeaders).length > 0) {
1933
+ requestConfig.headers = mergedHeaders;
1934
+ }
1935
+ if (body) {
1936
+ requestConfig.body = typeof body === "string" ? body : body.toString();
1937
+ }
1938
+ if (timeout) {
1939
+ requestConfig.timeout = timeout;
1940
+ }
1941
+
1942
+ // Start stream
1943
+ const streamHandle = this._lib.httpcloak_stream_request(this._handle, JSON.stringify(requestConfig));
1944
+ if (streamHandle < 0) {
1945
+ throw new HTTPCloakError("Failed to start streaming request");
1946
+ }
1947
+
1948
+ // Get metadata
1949
+ const metadataStr = this._lib.httpcloak_stream_get_metadata(streamHandle);
1950
+ if (!metadataStr) {
1951
+ this._lib.httpcloak_stream_close(streamHandle);
1952
+ throw new HTTPCloakError("Failed to get stream metadata");
1953
+ }
1954
+
1955
+ const metadata = JSON.parse(metadataStr);
1956
+ if (metadata.error) {
1957
+ this._lib.httpcloak_stream_close(streamHandle);
1958
+ throw new HTTPCloakError(metadata.error);
1959
+ }
1960
+
1961
+ return new StreamResponse(streamHandle, this._lib, metadata);
1962
+ }
1963
+
1964
+ // ===========================================================================
1965
+ // Fast-path Methods (Zero-copy for maximum performance)
1966
+ // ===========================================================================
1967
+
1968
+ /**
1969
+ * Perform a fast GET request with zero-copy buffer transfer.
1970
+ *
1971
+ * This method bypasses JSON serialization and base64 encoding for the response body,
1972
+ * copying data directly from Go's memory to a Node.js Buffer.
1973
+ *
1974
+ * Use this method for downloading large files when you need maximum throughput.
1975
+ *
1976
+ * @param {string} url - Request URL
1977
+ * @param {Object} [options] - Request options
1978
+ * @param {Object} [options.headers] - Custom headers
1979
+ * @param {Object} [options.params] - Query parameters
1980
+ * @param {Object} [options.cookies] - Cookies to send with this request
1981
+ * @param {Array} [options.auth] - Basic auth [username, password]
1982
+ * @returns {FastResponse} Fast response object with Buffer body
1983
+ *
1984
+ * Example:
1985
+ * const response = session.getFast("https://example.com/large-file.zip");
1986
+ * console.log(`Downloaded ${response.body.length} bytes`);
1987
+ * fs.writeFileSync("file.zip", response.body);
1988
+ */
1989
+ getFast(url, options = {}) {
1990
+ const { headers = null, params = null, cookies = null, auth = null } = options;
1991
+
1992
+ url = addParamsToUrl(url, params);
1993
+ let mergedHeaders = this._mergeHeaders(headers);
1994
+ // Use request auth if provided, otherwise fall back to session auth
1995
+ const effectiveAuth = auth !== null ? auth : this.auth;
1996
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
1997
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
1998
+
1999
+ // Build request options JSON with headers wrapper
2000
+ const reqOptions = {};
2001
+ if (mergedHeaders) {
2002
+ reqOptions.headers = mergedHeaders;
2003
+ }
2004
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2005
+
2006
+ const startTime = Date.now();
2007
+
2008
+ // Get raw response handle
2009
+ const responseHandle = this._lib.httpcloak_get_raw(this._handle, url, optionsJson);
2010
+ if (responseHandle === 0 || responseHandle === 0n) {
2011
+ throw new HTTPCloakError("Failed to make request");
2012
+ }
2013
+
2014
+ // Get body length first (1 FFI call)
2015
+ const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
2016
+ if (bodyLen < 0) {
2017
+ this._lib.httpcloak_response_free(responseHandle);
2018
+ throw new HTTPCloakError("Failed to get response body length");
2019
+ }
2020
+
2021
+ // Acquire pooled buffer
2022
+ const pooledBuffer = _bufferPool.acquire(bodyLen);
2023
+
2024
+ // Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
2025
+ const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
2026
+ if (!metadataStr) {
2027
+ _bufferPool.release(pooledBuffer);
2028
+ throw new HTTPCloakError("Failed to finalize response");
2029
+ }
2030
+
2031
+ const metadata = JSON.parse(metadataStr);
2032
+ if (metadata.error) {
2033
+ _bufferPool.release(pooledBuffer);
2034
+ throw new HTTPCloakError(metadata.error);
2035
+ }
2036
+
2037
+ // Create a view of just the used portion
2038
+ const buffer = pooledBuffer.subarray(0, bodyLen);
2039
+
2040
+ const elapsed = Date.now() - startTime;
2041
+ return new FastResponse(metadata, buffer, elapsed, pooledBuffer);
2042
+ }
2043
+
2044
+ /**
2045
+ * High-performance POST request optimized for large uploads.
2046
+ *
2047
+ * Uses binary buffer passing and response pooling for maximum throughput.
2048
+ * Call response.release() when done to return buffers to pool.
2049
+ *
2050
+ * @param {string} url - Request URL
2051
+ * @param {Object} [options] - Request options
2052
+ * @param {Buffer} [options.body] - Request body as Buffer
2053
+ * @param {Object} [options.headers] - Request headers
2054
+ * @param {Object} [options.params] - Query parameters
2055
+ * @param {Object} [options.cookies] - Cookies to send with this request
2056
+ * @param {Array} [options.auth] - Basic auth [username, password]
2057
+ * @returns {FastResponse} Fast response object with Buffer body
2058
+ *
2059
+ * Example:
2060
+ * const data = Buffer.alloc(10 * 1024 * 1024); // 10MB
2061
+ * const response = session.postFast("https://example.com/upload", { body: data });
2062
+ * console.log(`Uploaded, response: ${response.statusCode}`);
2063
+ * response.release();
2064
+ */
2065
+ postFast(url, options = {}) {
2066
+ let { body = null, headers = null, params = null, cookies = null, auth = null } = options;
2067
+
2068
+ url = addParamsToUrl(url, params);
2069
+ let mergedHeaders = this._mergeHeaders(headers);
2070
+ // Use request auth if provided, otherwise fall back to session auth
2071
+ const effectiveAuth = auth !== null ? auth : this.auth;
2072
+ mergedHeaders = applyAuth(mergedHeaders, effectiveAuth);
2073
+ mergedHeaders = this._applyCookies(mergedHeaders, cookies);
2074
+
2075
+ // Ensure body is a Buffer
2076
+ if (body === null) {
2077
+ body = Buffer.alloc(0);
2078
+ } else if (typeof body === "string") {
2079
+ body = Buffer.from(body, "utf8");
2080
+ } else if (!Buffer.isBuffer(body)) {
2081
+ throw new HTTPCloakError("postFast body must be a Buffer or string");
2082
+ }
2083
+
2084
+ // Build request options JSON with headers wrapper
2085
+ const reqOptions = {};
2086
+ if (mergedHeaders) {
2087
+ reqOptions.headers = mergedHeaders;
2088
+ }
2089
+ const optionsJson = Object.keys(reqOptions).length > 0 ? JSON.stringify(reqOptions) : null;
2090
+
2091
+ const startTime = Date.now();
2092
+
2093
+ // Use httpcloak_post_raw with binary buffer (no string conversion!)
2094
+ const responseHandle = this._lib.httpcloak_post_raw(this._handle, url, body, body.length, optionsJson);
2095
+ if (responseHandle === 0 || responseHandle === 0n || responseHandle < 0) {
2096
+ throw new HTTPCloakError("Failed to make POST request");
2097
+ }
2098
+
2099
+ // Get body length first (1 FFI call)
2100
+ const bodyLen = this._lib.httpcloak_response_get_body_len(responseHandle);
2101
+ if (bodyLen < 0) {
2102
+ this._lib.httpcloak_response_free(responseHandle);
2103
+ throw new HTTPCloakError("Failed to get response body length");
2104
+ }
2105
+
2106
+ // Acquire pooled buffer for response
2107
+ const pooledBuffer = _bufferPool.acquire(bodyLen);
2108
+
2109
+ // Finalize: copy body + get metadata + free handle (1 FFI call instead of 3)
2110
+ const metadataStr = this._lib.httpcloak_response_finalize(responseHandle, pooledBuffer, bodyLen);
2111
+ if (!metadataStr) {
2112
+ _bufferPool.release(pooledBuffer);
2113
+ throw new HTTPCloakError("Failed to finalize response");
2114
+ }
2115
+
2116
+ const metadata = JSON.parse(metadataStr);
2117
+ if (metadata.error) {
2118
+ _bufferPool.release(pooledBuffer);
2119
+ throw new HTTPCloakError(metadata.error);
2120
+ }
2121
+
2122
+ // Create a view of just the used portion
2123
+ const responseBuffer = pooledBuffer.subarray(0, bodyLen);
2124
+
2125
+ const elapsed = Date.now() - startTime;
2126
+ return new FastResponse(metadata, responseBuffer, elapsed, pooledBuffer);
2127
+ }
1208
2128
  }
1209
2129
 
1210
2130
  // =============================================================================
@@ -1384,6 +2304,8 @@ module.exports = {
1384
2304
  // Classes
1385
2305
  Session,
1386
2306
  Response,
2307
+ FastResponse,
2308
+ StreamResponse,
1387
2309
  Cookie,
1388
2310
  RedirectInfo,
1389
2311
  HTTPCloakError,