hanzio 1.0.2 → 1.0.6

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.
@@ -0,0 +1,552 @@
1
+ // src/p-queue/index.ts
2
+ class TimeoutError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "TimeoutError";
6
+ }
7
+ }
8
+
9
+ class PriorityQueue {
10
+ #queue = [];
11
+ enqueue(run, options = {}) {
12
+ const element = {
13
+ priority: options.priority ?? 0,
14
+ id: options.id,
15
+ run
16
+ };
17
+ const index = this.#queue.findIndex((item) => item.priority < element.priority);
18
+ if (index === -1) {
19
+ this.#queue.push(element);
20
+ return;
21
+ }
22
+ this.#queue.splice(index, 0, element);
23
+ }
24
+ dequeue() {
25
+ return this.#queue.shift()?.run;
26
+ }
27
+ filter(options) {
28
+ return this.#queue.filter((item) => Object.entries(options).every(([key, value]) => item[key] === value)).map((item) => item.run);
29
+ }
30
+ setPriority(id, priority) {
31
+ const index = this.#queue.findIndex((item2) => item2.id === id);
32
+ if (index === -1) {
33
+ throw new ReferenceError(`No promise function with the id "${id}" exists in the queue.`);
34
+ }
35
+ const [item] = this.#queue.splice(index, 1);
36
+ this.enqueue(item.run, { ...item, priority });
37
+ }
38
+ remove(id) {
39
+ const index = this.#queue.findIndex((item) => item.id === id);
40
+ if (index !== -1) {
41
+ this.#queue.splice(index, 1);
42
+ }
43
+ }
44
+ get size() {
45
+ return this.#queue.length;
46
+ }
47
+ }
48
+
49
+ class PQueue {
50
+ #listeners = new Map;
51
+ #queue;
52
+ #queueClass;
53
+ #pending = 0;
54
+ #concurrency;
55
+ #isPaused;
56
+ #intervalCap;
57
+ #interval;
58
+ #carryoverIntervalCount;
59
+ #strict;
60
+ #intervalCount = 0;
61
+ #intervalEnd = 0;
62
+ #intervalTimer;
63
+ #strictTimestamps = [];
64
+ #rateLimited = false;
65
+ #idCounter = 1n;
66
+ #runningTasks = new Map;
67
+ timeout;
68
+ constructor(options = {}) {
69
+ const {
70
+ concurrency = Number.POSITIVE_INFINITY,
71
+ autoStart = true,
72
+ queueClass = PriorityQueue,
73
+ intervalCap = Number.POSITIVE_INFINITY,
74
+ interval = 0,
75
+ carryoverIntervalCount,
76
+ carryoverConcurrencyCount,
77
+ strict = false,
78
+ timeout
79
+ } = options;
80
+ if (!(typeof concurrency === "number" && concurrency >= 1)) {
81
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
82
+ }
83
+ if (!(typeof intervalCap === "number" && intervalCap >= 1)) {
84
+ throw new TypeError("Expected `intervalCap` to be a number from 1 and up");
85
+ }
86
+ if (!(typeof interval === "number" && Number.isFinite(interval) && interval >= 0)) {
87
+ throw new TypeError("Expected `interval` to be a finite number >= 0");
88
+ }
89
+ if (strict && interval === 0) {
90
+ throw new TypeError("The `strict` option requires a non-zero `interval`");
91
+ }
92
+ if (strict && intervalCap === Number.POSITIVE_INFINITY) {
93
+ throw new TypeError("The `strict` option requires a finite `intervalCap`");
94
+ }
95
+ if (timeout !== undefined && !(Number.isFinite(timeout) && timeout > 0)) {
96
+ throw new TypeError("Expected `timeout` to be a positive finite number");
97
+ }
98
+ this.#queueClass = queueClass;
99
+ this.#queue = new queueClass;
100
+ this.#concurrency = concurrency;
101
+ this.#isPaused = !autoStart;
102
+ this.#intervalCap = intervalCap;
103
+ this.#interval = interval;
104
+ this.#carryoverIntervalCount = carryoverIntervalCount ?? carryoverConcurrencyCount ?? false;
105
+ this.#strict = strict;
106
+ this.timeout = timeout;
107
+ }
108
+ on(event, listener) {
109
+ const listeners = this.#listeners.get(event) ?? new Set;
110
+ listeners.add(listener);
111
+ this.#listeners.set(event, listeners);
112
+ return this;
113
+ }
114
+ off(event, listener) {
115
+ this.#listeners.get(event)?.delete(listener);
116
+ return this;
117
+ }
118
+ emit(event, ...arguments_) {
119
+ const listeners = this.#listeners.get(event);
120
+ if (!listeners || listeners.size === 0) {
121
+ return false;
122
+ }
123
+ for (const listener of [...listeners]) {
124
+ listener(...arguments_);
125
+ }
126
+ return true;
127
+ }
128
+ get concurrency() {
129
+ return this.#concurrency;
130
+ }
131
+ set concurrency(newConcurrency) {
132
+ if (!(typeof newConcurrency === "number" && newConcurrency >= 1)) {
133
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
134
+ }
135
+ this.#concurrency = newConcurrency;
136
+ this.#processQueue();
137
+ }
138
+ get size() {
139
+ return this.#queue.size;
140
+ }
141
+ sizeBy(options) {
142
+ return this.#queue.filter(options).length;
143
+ }
144
+ get pending() {
145
+ return this.#pending;
146
+ }
147
+ get isPaused() {
148
+ return this.#isPaused;
149
+ }
150
+ get isRateLimited() {
151
+ this.#updateRateLimitState();
152
+ return this.#rateLimited;
153
+ }
154
+ get isSaturated() {
155
+ return this.#pending >= this.#concurrency && this.#queue.size > 0 || this.isRateLimited && this.#queue.size > 0;
156
+ }
157
+ get runningTasks() {
158
+ return [...this.#runningTasks.values()].map((task) => ({ ...task }));
159
+ }
160
+ setPriority(id, priority) {
161
+ if (typeof priority !== "number" || !Number.isFinite(priority)) {
162
+ throw new TypeError("Expected `priority` to be a finite number");
163
+ }
164
+ this.#queue.setPriority(id, priority);
165
+ }
166
+ async add(function_, options = {}) {
167
+ const taskOptions = {
168
+ timeout: this.timeout,
169
+ ...options,
170
+ id: options.id ?? (this.#idCounter++).toString()
171
+ };
172
+ return new Promise((resolve, reject) => {
173
+ const taskSymbol = Symbol(taskOptions.id);
174
+ let didRun = false;
175
+ let queuedAbortHandler;
176
+ let runningAbortHandler;
177
+ const cleanupQueuedAbort = () => {
178
+ if (queuedAbortHandler) {
179
+ taskOptions.signal?.removeEventListener("abort", queuedAbortHandler);
180
+ queuedAbortHandler = undefined;
181
+ }
182
+ };
183
+ const run = async () => {
184
+ didRun = true;
185
+ cleanupQueuedAbort();
186
+ this.#pending++;
187
+ this.#runningTasks.set(taskSymbol, {
188
+ id: taskOptions.id,
189
+ priority: taskOptions.priority ?? 0,
190
+ startTime: Date.now(),
191
+ timeout: taskOptions.timeout
192
+ });
193
+ try {
194
+ taskOptions.signal?.throwIfAborted();
195
+ const operation = Promise.resolve(function_({ signal: taskOptions.signal }));
196
+ const result = await this.#withTimeoutAndAbort(operation, taskOptions, (listener) => {
197
+ runningAbortHandler = listener;
198
+ });
199
+ resolve(result);
200
+ this.emit("completed", result);
201
+ } catch (error) {
202
+ reject(error);
203
+ this.emit("error", error);
204
+ } finally {
205
+ if (runningAbortHandler) {
206
+ taskOptions.signal?.removeEventListener("abort", runningAbortHandler);
207
+ }
208
+ this.#runningTasks.delete(taskSymbol);
209
+ queueMicrotask(() => {
210
+ this.#next();
211
+ });
212
+ }
213
+ };
214
+ if (taskOptions.signal?.aborted) {
215
+ reject(taskOptions.signal.reason);
216
+ return;
217
+ }
218
+ if (taskOptions.signal) {
219
+ queuedAbortHandler = () => {
220
+ if (didRun) {
221
+ return;
222
+ }
223
+ this.#queue.remove?.(taskOptions.id);
224
+ reject(taskOptions.signal?.reason);
225
+ this.emit("next");
226
+ this.#tryToStartAnother();
227
+ };
228
+ taskOptions.signal.addEventListener("abort", queuedAbortHandler, {
229
+ once: true
230
+ });
231
+ }
232
+ this.#queue.enqueue(run, taskOptions);
233
+ this.emit("add");
234
+ this.#processQueue();
235
+ });
236
+ }
237
+ async addAll(functions, options) {
238
+ return Promise.all(functions.map(async (function_) => this.add(function_, options)));
239
+ }
240
+ start() {
241
+ if (!this.#isPaused) {
242
+ return this;
243
+ }
244
+ this.#isPaused = false;
245
+ this.#processQueue();
246
+ return this;
247
+ }
248
+ pause() {
249
+ this.#isPaused = true;
250
+ }
251
+ clear() {
252
+ this.#queue = new this.#queueClass;
253
+ this.#clearIntervalTimer();
254
+ this.#updateRateLimitState();
255
+ this.emit("empty");
256
+ if (this.#pending === 0) {
257
+ this.emit("idle");
258
+ }
259
+ this.emit("next");
260
+ }
261
+ async onEmpty() {
262
+ if (this.#queue.size === 0) {
263
+ return;
264
+ }
265
+ await this.#onEvent("empty");
266
+ }
267
+ async onIdle() {
268
+ if (this.#queue.size === 0 && this.#pending === 0) {
269
+ return;
270
+ }
271
+ await this.#onEvent("idle");
272
+ }
273
+ async onPendingZero() {
274
+ if (this.#pending === 0) {
275
+ return;
276
+ }
277
+ await this.#onEvent("pendingZero");
278
+ }
279
+ async onRateLimit() {
280
+ if (this.isRateLimited) {
281
+ return;
282
+ }
283
+ await this.#onEvent("rateLimit");
284
+ }
285
+ async onRateLimitCleared() {
286
+ if (!this.isRateLimited) {
287
+ return;
288
+ }
289
+ await this.#onEvent("rateLimitCleared");
290
+ }
291
+ onError() {
292
+ return new Promise((_resolve, reject) => {
293
+ const listener = (error) => {
294
+ this.off("error", listener);
295
+ reject(error);
296
+ };
297
+ this.on("error", listener);
298
+ });
299
+ }
300
+ async onSizeLessThan(limit) {
301
+ if (this.#queue.size < limit) {
302
+ return;
303
+ }
304
+ await this.#onEvent("next", () => this.#queue.size < limit);
305
+ }
306
+ async#onEvent(event, filter) {
307
+ return new Promise((resolve) => {
308
+ const listener = () => {
309
+ if (filter && !filter()) {
310
+ return;
311
+ }
312
+ this.off(event, listener);
313
+ resolve();
314
+ };
315
+ this.on(event, listener);
316
+ });
317
+ }
318
+ async#withTimeoutAndAbort(operation, options, setAbortListener) {
319
+ const races = [operation];
320
+ let timeoutId;
321
+ if (options.timeout) {
322
+ races.push(new Promise((_resolve, reject) => {
323
+ timeoutId = setTimeout(() => {
324
+ reject(new TimeoutError(`Task timed out after ${options.timeout}ms`));
325
+ }, options.timeout);
326
+ }));
327
+ }
328
+ if (options.signal) {
329
+ races.push(new Promise((_resolve, reject) => {
330
+ const listener = () => {
331
+ reject(options.signal?.reason);
332
+ };
333
+ setAbortListener(listener);
334
+ options.signal?.addEventListener("abort", listener, { once: true });
335
+ }));
336
+ }
337
+ try {
338
+ return await Promise.race(races);
339
+ } finally {
340
+ if (timeoutId) {
341
+ clearTimeout(timeoutId);
342
+ }
343
+ }
344
+ }
345
+ #next() {
346
+ this.#pending--;
347
+ if (this.#pending === 0) {
348
+ this.emit("pendingZero");
349
+ }
350
+ this.#processQueue();
351
+ this.emit("next");
352
+ }
353
+ #processQueue() {
354
+ while (this.#tryToStartAnother()) {}
355
+ this.#updateRateLimitState();
356
+ }
357
+ #tryToStartAnother() {
358
+ if (this.#queue.size === 0) {
359
+ this.#clearIntervalTimer();
360
+ this.emit("empty");
361
+ if (this.#pending === 0) {
362
+ this.emit("idle");
363
+ }
364
+ return false;
365
+ }
366
+ if (this.#isPaused || this.#pending >= this.#concurrency || !this.#doesIntervalAllowAnother()) {
367
+ return false;
368
+ }
369
+ const job = this.#queue.dequeue();
370
+ if (!job) {
371
+ return false;
372
+ }
373
+ this.#consumeIntervalSlot();
374
+ this.emit("active");
375
+ job();
376
+ return true;
377
+ }
378
+ #doesIntervalAllowAnother() {
379
+ if (this.#intervalCap === Number.POSITIVE_INFINITY || this.#interval === 0) {
380
+ return true;
381
+ }
382
+ const now = Date.now();
383
+ if (this.#strict) {
384
+ this.#strictTimestamps = this.#strictTimestamps.filter((timestamp) => now - timestamp < this.#interval);
385
+ if (this.#strictTimestamps.length < this.#intervalCap) {
386
+ return true;
387
+ }
388
+ this.#scheduleIntervalResume(this.#interval - (now - this.#strictTimestamps[0]));
389
+ return false;
390
+ }
391
+ if (this.#intervalEnd === 0 || now >= this.#intervalEnd) {
392
+ this.#intervalCount = this.#carryoverIntervalCount ? this.#pending : 0;
393
+ this.#intervalEnd = now + this.#interval;
394
+ }
395
+ if (this.#intervalCount < this.#intervalCap) {
396
+ return true;
397
+ }
398
+ this.#scheduleIntervalResume(this.#intervalEnd - now);
399
+ return false;
400
+ }
401
+ #consumeIntervalSlot() {
402
+ if (this.#intervalCap === Number.POSITIVE_INFINITY || this.#interval === 0) {
403
+ return;
404
+ }
405
+ if (this.#strict) {
406
+ this.#strictTimestamps.push(Date.now());
407
+ return;
408
+ }
409
+ this.#intervalCount++;
410
+ }
411
+ #scheduleIntervalResume(delay) {
412
+ if (this.#intervalTimer) {
413
+ return;
414
+ }
415
+ this.#intervalTimer = setTimeout(() => {
416
+ this.#intervalTimer = undefined;
417
+ this.#processQueue();
418
+ }, Math.max(0, delay));
419
+ }
420
+ #clearIntervalTimer() {
421
+ if (this.#intervalTimer) {
422
+ clearTimeout(this.#intervalTimer);
423
+ this.#intervalTimer = undefined;
424
+ }
425
+ }
426
+ #updateRateLimitState() {
427
+ const previous = this.#rateLimited;
428
+ const isRateLimited = this.#queue.size > 0 && this.#intervalCap !== Number.POSITIVE_INFINITY && this.#interval > 0 && !this.#doesIntervalAllowAnother();
429
+ this.#rateLimited = isRateLimited;
430
+ if (previous !== isRateLimited) {
431
+ this.emit(isRateLimited ? "rateLimit" : "rateLimitCleared");
432
+ }
433
+ }
434
+ }
435
+
436
+ // src/sitemap/index.ts
437
+ var createTimeoutController = (timeout) => {
438
+ const controller = new AbortController;
439
+ setTimeout(() => controller.abort(), timeout);
440
+ return controller;
441
+ };
442
+ var discoverSitemaps = async (domain) => {
443
+ const normalizedDomain = domain.replace(/\/$/, "");
444
+ const commonLocations = [
445
+ "/sitemap.xml",
446
+ "/sitemap_index.xml",
447
+ "/sitemap/",
448
+ "/sitemaps.xml",
449
+ "/sitemap/sitemap.xml"
450
+ ];
451
+ const sitemaps = new Set;
452
+ const requestOptions = {
453
+ headers: {
454
+ "User-Agent": "SitemapCrawler/1.0"
455
+ },
456
+ signal: createTimeoutController(5000).signal
457
+ };
458
+ try {
459
+ const robotsTxt = await fetch(`${normalizedDomain}/robots.txt`, {
460
+ ...requestOptions
461
+ });
462
+ if (robotsTxt.ok) {
463
+ const text = await robotsTxt.text();
464
+ const sitemapMatches = text.match(/Sitemap: (.*)/gi);
465
+ if (sitemapMatches) {
466
+ sitemapMatches.forEach((match) => {
467
+ const url = match.replace("Sitemap:", "").trim();
468
+ sitemaps.add(url);
469
+ });
470
+ }
471
+ if (sitemaps.size > 0) {
472
+ return [...sitemaps];
473
+ }
474
+ }
475
+ } catch (error) {
476
+ console.warn("Could not fetch robots.txt:", error instanceof Error ? error.message : "Unknown error");
477
+ }
478
+ for (const location of commonLocations) {
479
+ try {
480
+ const response = await fetch(`${normalizedDomain}${location}`, {
481
+ ...requestOptions,
482
+ method: "HEAD"
483
+ });
484
+ if (response.ok) {
485
+ return [`${normalizedDomain}${location}`];
486
+ }
487
+ } catch (_error) {}
488
+ }
489
+ return [];
490
+ };
491
+ var getDomainSitemap = async (domain, options = {}) => {
492
+ const {
493
+ filterIndexes,
494
+ concurrency = 10,
495
+ maxDepth = 5,
496
+ timeout = 1e4
497
+ } = options;
498
+ let normalizedDomain = domain.replace(/\/$/, "");
499
+ if (!normalizedDomain.startsWith("http")) {
500
+ normalizedDomain = `https://${normalizedDomain}`;
501
+ }
502
+ const sitemapUrls = await discoverSitemaps(normalizedDomain);
503
+ if (sitemapUrls.length === 0) {
504
+ throw new Error("No sitemaps found for the domain");
505
+ }
506
+ const queue = new PQueue({ concurrency });
507
+ const result = {
508
+ links: new Set,
509
+ visited: new Set
510
+ };
511
+ const locRegex = /<loc>([^<]+)<\/loc>/g;
512
+ const isXmlSitemap = (url) => url.endsWith(".xml") || url.toLowerCase().includes("sitemap");
513
+ const processSitemap = async (sitemapUrl, depth = 0) => {
514
+ if (depth > maxDepth || result.visited.has(sitemapUrl)) {
515
+ return;
516
+ }
517
+ result.visited.add(sitemapUrl);
518
+ try {
519
+ const controller = createTimeoutController(timeout);
520
+ const response = await fetch(sitemapUrl, {
521
+ headers: {
522
+ "User-Agent": "SitemapCrawler/1.0"
523
+ },
524
+ signal: controller.signal
525
+ });
526
+ if (!response.ok)
527
+ throw new Error(`HTTP error! status: ${response.status}`);
528
+ const data = await response.text();
529
+ const matches = [...data.matchAll(locRegex)].map((match) => match[1].trim()).filter((link) => {
530
+ if (filterIndexes && isXmlSitemap(link)) {
531
+ return link.includes(filterIndexes);
532
+ }
533
+ return true;
534
+ });
535
+ for (const link of matches) {
536
+ if (isXmlSitemap(link)) {
537
+ queue.add(() => processSitemap(link, depth + 1));
538
+ } else {
539
+ result.links.add(link);
540
+ }
541
+ }
542
+ } catch (error) {
543
+ console.warn(`Failed to process sitemap ${sitemapUrl}:`, error instanceof Error ? error.message : "Unknown error");
544
+ }
545
+ };
546
+ await Promise.all(sitemapUrls.map((sitemapUrl) => queue.add(() => processSitemap(sitemapUrl))));
547
+ await queue.onIdle();
548
+ return [...result.links];
549
+ };
550
+ export {
551
+ getDomainSitemap
552
+ };