@duyquangnvx/webnovel-downloader 0.4.0 → 0.4.2

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/dist/index.cjs CHANGED
@@ -473,6 +473,9 @@ var silentLogger = createLogger({ level: "silent" });
473
473
  // src/index.ts
474
474
  init_errors();
475
475
 
476
+ // src/core/downloader.ts
477
+ var import_undici2 = require("undici");
478
+
476
479
  // src/core/registry.ts
477
480
  init_errors();
478
481
  var AdapterRegistry = class {
@@ -548,33 +551,46 @@ var EventBus = class {
548
551
  // src/http/client.ts
549
552
  var import_undici = require("undici");
550
553
  init_errors();
554
+ init_primitives();
551
555
  var DEFAULT_TIMEOUT_MS = 3e4;
556
+ var MAX_REDIRECTS = 5;
557
+ var REDIRECT_STATUSES = /* @__PURE__ */ new Set([301, 302, 303, 307, 308]);
552
558
  var UndiciHttpClient = class {
553
559
  #logger;
560
+ #dispatcher;
561
+ #ownsDispatcher;
554
562
  constructor(opts) {
555
563
  this.#logger = opts.logger;
564
+ this.#ownsDispatcher = opts.dispatcher === void 0;
565
+ this.#dispatcher = opts.dispatcher ?? new import_undici.Agent();
556
566
  }
557
567
  async get(url, opts) {
558
568
  const timeoutMs = opts?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
559
569
  const half = Math.max(1, Math.floor(timeoutMs / 2));
560
570
  let res;
561
571
  try {
562
- res = await (0, import_undici.request)(url, {
563
- method: "GET",
564
- ...opts?.headers !== void 0 ? { headers: opts.headers } : {},
565
- ...opts?.signal !== void 0 ? { signal: opts.signal } : {},
566
- headersTimeout: half,
567
- bodyTimeout: half,
568
- maxRedirections: 5
569
- });
572
+ let currentUrl = url;
573
+ for (let hop = 0; ; hop++) {
574
+ res = await (0, import_undici.request)(currentUrl, {
575
+ method: "GET",
576
+ dispatcher: this.#dispatcher,
577
+ ...opts?.headers !== void 0 ? { headers: opts.headers } : {},
578
+ ...opts?.signal !== void 0 ? { signal: opts.signal } : {},
579
+ headersTimeout: half,
580
+ bodyTimeout: half
581
+ });
582
+ const next = hop < MAX_REDIRECTS ? redirectTarget(res.statusCode, res.headers, currentUrl) : void 0;
583
+ if (next === void 0) break;
584
+ await res.body.dump();
585
+ currentUrl = next;
586
+ }
570
587
  const body = await res.body.text();
571
588
  const headers = normalizeHeaders(res.headers);
572
- const finalUrl = url;
573
589
  return {
574
590
  status: res.statusCode,
575
591
  headers,
576
592
  body,
577
- url: finalUrl
593
+ url: currentUrl
578
594
  };
579
595
  } catch (cause) {
580
596
  if (res !== void 0) await res.body.dump().catch(() => {
@@ -585,11 +601,27 @@ var UndiciHttpClient = class {
585
601
  if (cause instanceof import_undici.errors.HeadersTimeoutError || cause instanceof import_undici.errors.BodyTimeoutError) {
586
602
  throw new TimeoutError(url, { cause });
587
603
  }
588
- this.#logger.debug({ url, cause }, "undici network error");
589
- throw new HttpError(0, url, "Network error", { cause });
604
+ const detail = cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause);
605
+ this.#logger.warn({ url, cause }, "undici network error");
606
+ throw new HttpError(0, url, `Network error (${detail})`, { cause });
590
607
  }
591
608
  }
609
+ /** Closes the dispatcher this client created. A no-op when one was injected — the caller owns that. */
610
+ async dispose() {
611
+ if (this.#ownsDispatcher) await this.#dispatcher.close();
612
+ }
592
613
  };
614
+ function redirectTarget(status, headers, currentUrl) {
615
+ if (!REDIRECT_STATUSES.has(status)) return void 0;
616
+ const raw = headers["location"];
617
+ const location = Array.isArray(raw) ? raw[0] : raw;
618
+ if (location === void 0 || location === "") return void 0;
619
+ try {
620
+ return unsafeBrandUrl(new URL(location, currentUrl).href);
621
+ } catch {
622
+ return void 0;
623
+ }
624
+ }
593
625
  function normalizeHeaders(h) {
594
626
  const out = {};
595
627
  for (const [k, v] of Object.entries(h)) {
@@ -720,9 +752,13 @@ var RateLimiter = class {
720
752
  #buckets = /* @__PURE__ */ new Map();
721
753
  #pauses = /* @__PURE__ */ new Map();
722
754
  constructor(opts = DEFAULT_RATE_LIMIT) {
755
+ const burst = opts.burst ?? Math.max(1, opts.requestsPerSecond);
756
+ if (burst < 1) {
757
+ throw new RangeError(`RateLimiter: burst must be >= 1 (got ${burst})`);
758
+ }
723
759
  this.#opts = {
724
760
  requestsPerSecond: opts.requestsPerSecond,
725
- burst: opts.burst ?? opts.requestsPerSecond
761
+ burst
726
762
  };
727
763
  }
728
764
  async acquire(host, signal) {
@@ -967,7 +1003,10 @@ function normalizeTransport(t) {
967
1003
  function buildHttpClient(opts = {}) {
968
1004
  const logger = opts.logger ?? silentLogger;
969
1005
  const cookieJar = opts.cookieJar ?? new CookieJar();
970
- const leaf = opts.undiciOverride ?? new UndiciHttpClient({ logger });
1006
+ const leaf = opts.undiciOverride ?? new UndiciHttpClient({
1007
+ logger,
1008
+ ...opts.dispatcher !== void 0 ? { dispatcher: opts.dispatcher } : {}
1009
+ });
971
1010
  const limiter = opts.rateLimiter ?? new RateLimiter(opts.rateLimit ?? DEFAULT_RATE_LIMIT);
972
1011
  const httpBranch = HttpStack.from(leaf).rotateUserAgent(opts.userAgents ?? new UserAgents(), opts.defaultHeaders, cookieJar).rateLimit(limiter, opts.onEvent).retry(normalizeRetry(opts.retry), logger, limiter, opts.onEvent).build();
973
1012
  const { mode } = normalizeTransport(opts.transport);
@@ -2104,6 +2143,7 @@ var Downloader = class {
2104
2143
  #transport;
2105
2144
  #cookieJar;
2106
2145
  #browserPool;
2146
+ #httpDispatcher;
2107
2147
  #transportListeners = /* @__PURE__ */ new Set();
2108
2148
  /**
2109
2149
  * Build a downloader from a set of adapters. `rateLimit`, `retry`, and
@@ -2138,10 +2178,12 @@ var Downloader = class {
2138
2178
  await handle.release();
2139
2179
  }
2140
2180
  } : void 0;
2181
+ this.#httpDispatcher = opts.http === void 0 && opts.undiciOverride === void 0 ? new import_undici2.Agent() : void 0;
2141
2182
  this.#http = opts.http ?? buildHttpClient({
2142
2183
  logger: this.#logger,
2143
2184
  transport: this.#transport,
2144
2185
  cookieJar: this.#cookieJar,
2186
+ ...this.#httpDispatcher !== void 0 ? { dispatcher: this.#httpDispatcher } : {},
2145
2187
  ...opts.rateLimit !== void 0 ? { rateLimit: opts.rateLimit } : {},
2146
2188
  ...opts.retry !== void 0 ? { retry: opts.retry } : {},
2147
2189
  onEvent: (e) => {
@@ -2172,10 +2214,11 @@ var Downloader = class {
2172
2214
  ...onCookieCapture !== void 0 ? { onCookieCapture } : {}
2173
2215
  });
2174
2216
  }
2175
- /** Release held resources (closes the browser pool, if any). Safe to call once when done. */
2217
+ /** Release held resources (closes the browser pool and HTTP dispatcher, if any). Safe to call once when done. */
2176
2218
  async dispose() {
2177
2219
  const pool = this.#browserPool;
2178
2220
  if (pool) await pool.dispose();
2221
+ if (this.#httpDispatcher) await this.#httpDispatcher.close();
2179
2222
  }
2180
2223
  /** True when a built-in/registered adapter can handle this URL. */
2181
2224
  canHandle(url) {