rezo 1.0.42 → 1.0.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/dist/adapters/curl.cjs +131 -29
  2. package/dist/adapters/curl.js +131 -29
  3. package/dist/adapters/entries/curl.d.ts +65 -0
  4. package/dist/adapters/entries/fetch.d.ts +65 -0
  5. package/dist/adapters/entries/http.d.ts +65 -0
  6. package/dist/adapters/entries/http2.d.ts +65 -0
  7. package/dist/adapters/entries/react-native.d.ts +65 -0
  8. package/dist/adapters/entries/xhr.d.ts +65 -0
  9. package/dist/adapters/http2.cjs +209 -22
  10. package/dist/adapters/http2.js +209 -22
  11. package/dist/adapters/index.cjs +6 -6
  12. package/dist/cache/file-cacher.cjs +7 -1
  13. package/dist/cache/file-cacher.js +7 -1
  14. package/dist/cache/index.cjs +15 -13
  15. package/dist/cache/index.js +1 -0
  16. package/dist/cache/navigation-history.cjs +298 -0
  17. package/dist/cache/navigation-history.js +296 -0
  18. package/dist/cache/url-store.cjs +7 -1
  19. package/dist/cache/url-store.js +7 -1
  20. package/dist/core/rezo.cjs +7 -0
  21. package/dist/core/rezo.js +7 -0
  22. package/dist/crawler.d.ts +196 -11
  23. package/dist/entries/crawler.cjs +5 -5
  24. package/dist/index.cjs +27 -24
  25. package/dist/index.d.ts +73 -0
  26. package/dist/index.js +1 -0
  27. package/dist/internal/agents/base.cjs +113 -0
  28. package/dist/internal/agents/base.js +110 -0
  29. package/dist/internal/agents/http-proxy.cjs +89 -0
  30. package/dist/internal/agents/http-proxy.js +86 -0
  31. package/dist/internal/agents/https-proxy.cjs +176 -0
  32. package/dist/internal/agents/https-proxy.js +173 -0
  33. package/dist/internal/agents/index.cjs +10 -0
  34. package/dist/internal/agents/index.js +5 -0
  35. package/dist/internal/agents/socks-client.cjs +571 -0
  36. package/dist/internal/agents/socks-client.js +567 -0
  37. package/dist/internal/agents/socks-proxy.cjs +75 -0
  38. package/dist/internal/agents/socks-proxy.js +72 -0
  39. package/dist/platform/browser.d.ts +65 -0
  40. package/dist/platform/bun.d.ts +65 -0
  41. package/dist/platform/deno.d.ts +65 -0
  42. package/dist/platform/node.d.ts +65 -0
  43. package/dist/platform/react-native.d.ts +65 -0
  44. package/dist/platform/worker.d.ts +65 -0
  45. package/dist/plugin/crawler-options.cjs +1 -1
  46. package/dist/plugin/crawler-options.js +1 -1
  47. package/dist/plugin/crawler.cjs +192 -1
  48. package/dist/plugin/crawler.js +192 -1
  49. package/dist/plugin/index.cjs +36 -36
  50. package/dist/proxy/index.cjs +18 -16
  51. package/dist/proxy/index.js +17 -12
  52. package/dist/queue/index.cjs +8 -8
  53. package/dist/responses/buildError.cjs +11 -2
  54. package/dist/responses/buildError.js +11 -2
  55. package/dist/responses/universal/index.cjs +11 -11
  56. package/dist/utils/curl.cjs +317 -0
  57. package/dist/utils/curl.js +314 -0
  58. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import * as http2 from "node:http2";
2
+ import * as tls from "node:tls";
2
3
  import * as zlib from "node:zlib";
3
4
  import { URL } from "node:url";
4
5
  import { Readable } from "node:stream";
@@ -14,6 +15,8 @@ import { DownloadResponse } from '../responses/download.js';
14
15
  import { UploadResponse } from '../responses/upload.js';
15
16
  import { CompressionUtil } from '../utils/compression.js';
16
17
  import { isSameDomain, RezoPerformance } from '../utils/tools.js';
18
+ import { SocksClient } from '../internal/agents/socks-client.js';
19
+ import * as net from "node:net";
17
20
  import { ResponseCache } from '../cache/response-cache.js';
18
21
  let zstdDecompressSync = null;
19
22
  let zstdChecked = false;
@@ -194,8 +197,9 @@ class Http2SessionPool {
194
197
  this.cleanupInterval.unref();
195
198
  }
196
199
  }
197
- getSessionKey(url, options) {
198
- return `${url.protocol}//${url.host}`;
200
+ getSessionKey(url, options, proxy) {
201
+ const proxyKey = proxy ? typeof proxy === "string" ? proxy : `${proxy.protocol}://${proxy.host}:${proxy.port}` : "";
202
+ return `${url.protocol}//${url.host}${proxyKey ? `@${proxyKey}` : ""}`;
199
203
  }
200
204
  isSessionHealthy(session, entry) {
201
205
  if (session.closed || session.destroyed)
@@ -207,8 +211,8 @@ class Http2SessionPool {
207
211
  return false;
208
212
  return true;
209
213
  }
210
- async getSession(url, options, timeout, forceNew = false) {
211
- const key = this.getSessionKey(url, options);
214
+ async getSession(url, options, timeout, forceNew = false, proxy) {
215
+ const key = this.getSessionKey(url, options, proxy);
212
216
  const existing = this.sessions.get(key);
213
217
  if (!forceNew && existing && this.isSessionHealthy(existing.session, existing)) {
214
218
  existing.lastUsed = Date.now();
@@ -221,12 +225,13 @@ class Http2SessionPool {
221
225
  } catch {}
222
226
  this.sessions.delete(key);
223
227
  }
224
- const session = await this.createSession(url, options, timeout);
228
+ const session = await this.createSession(url, options, timeout, proxy);
225
229
  const entry = {
226
230
  session,
227
231
  lastUsed: Date.now(),
228
232
  refCount: 1,
229
- goawayReceived: false
233
+ goawayReceived: false,
234
+ proxy
230
235
  };
231
236
  this.sessions.set(key, entry);
232
237
  session.on("close", () => {
@@ -240,15 +245,19 @@ class Http2SessionPool {
240
245
  });
241
246
  return session;
242
247
  }
243
- createSession(url, options, timeout) {
248
+ async createSession(url, options, timeout, proxy) {
249
+ const authority = `${url.protocol}//${url.host}`;
250
+ const sessionOptions = {
251
+ ...options,
252
+ rejectUnauthorized: options?.rejectUnauthorized !== false,
253
+ ALPNProtocols: ["h2", "http/1.1"],
254
+ timeout
255
+ };
256
+ if (proxy) {
257
+ const tunnelSocket = await this.createProxyTunnel(url, proxy, timeout, options?.rejectUnauthorized);
258
+ sessionOptions.createConnection = () => tunnelSocket;
259
+ }
244
260
  return new Promise((resolve, reject) => {
245
- const authority = `${url.protocol}//${url.host}`;
246
- const sessionOptions = {
247
- ...options,
248
- rejectUnauthorized: options?.rejectUnauthorized !== false,
249
- ALPNProtocols: ["h2", "http/1.1"],
250
- timeout
251
- };
252
261
  const session = http2.connect(authority, sessionOptions);
253
262
  let settled = false;
254
263
  const timeoutId = timeout ? setTimeout(() => {
@@ -279,8 +288,186 @@ class Http2SessionPool {
279
288
  });
280
289
  });
281
290
  }
282
- releaseSession(url) {
283
- const key = this.getSessionKey(url);
291
+ async createProxyTunnel(url, proxy, timeout, rejectUnauthorized) {
292
+ return new Promise((resolve, reject) => {
293
+ let proxyUrl;
294
+ let proxyAuth;
295
+ if (typeof proxy === "string") {
296
+ proxyUrl = new URL(proxy);
297
+ if (proxyUrl.username || proxyUrl.password) {
298
+ proxyAuth = Buffer.from(`${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`).toString("base64");
299
+ }
300
+ } else {
301
+ const protocol = proxy.protocol || "http";
302
+ let proxyUrlStr = `${protocol}://${proxy.host}:${proxy.port}`;
303
+ if (proxy.auth) {
304
+ const encodedUser = encodeURIComponent(proxy.auth.username);
305
+ const encodedPass = encodeURIComponent(proxy.auth.password);
306
+ proxyUrlStr = `${protocol}://${encodedUser}:${encodedPass}@${proxy.host}:${proxy.port}`;
307
+ proxyAuth = Buffer.from(`${proxy.auth.username}:${proxy.auth.password}`).toString("base64");
308
+ }
309
+ proxyUrl = new URL(proxyUrlStr);
310
+ }
311
+ const targetHost = url.hostname;
312
+ const targetPort = url.port || (url.protocol === "https:" ? "443" : "80");
313
+ if (proxyUrl.protocol.startsWith("socks")) {
314
+ const socksType = proxyUrl.protocol === "socks5:" || proxyUrl.protocol === "socks5h:" ? 5 : 4;
315
+ const socksOpts = {
316
+ proxy: {
317
+ host: proxyUrl.hostname,
318
+ port: parseInt(proxyUrl.port || "1080", 10),
319
+ type: socksType,
320
+ userId: proxyUrl.username ? decodeURIComponent(proxyUrl.username) : undefined,
321
+ password: proxyUrl.password ? decodeURIComponent(proxyUrl.password) : undefined
322
+ },
323
+ destination: {
324
+ host: targetHost,
325
+ port: parseInt(targetPort, 10)
326
+ },
327
+ command: "connect",
328
+ timeout
329
+ };
330
+ SocksClient.createConnection(socksOpts).then(({ socket }) => {
331
+ if (url.protocol === "https:") {
332
+ const tlsSocket = tls.connect({
333
+ socket,
334
+ host: targetHost,
335
+ servername: targetHost,
336
+ rejectUnauthorized: rejectUnauthorized !== false,
337
+ ALPNProtocols: ["h2", "http/1.1"]
338
+ });
339
+ const tlsTimeoutId = timeout ? setTimeout(() => {
340
+ tlsSocket.destroy();
341
+ reject(new Error(`TLS handshake timeout after ${timeout}ms`));
342
+ }, timeout) : null;
343
+ tlsSocket.on("secureConnect", () => {
344
+ if (tlsTimeoutId)
345
+ clearTimeout(tlsTimeoutId);
346
+ const alpn = tlsSocket.alpnProtocol;
347
+ if (alpn && alpn !== "h2") {
348
+ tlsSocket.destroy();
349
+ reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
350
+ return;
351
+ }
352
+ resolve(tlsSocket);
353
+ });
354
+ tlsSocket.on("error", (err) => {
355
+ if (tlsTimeoutId)
356
+ clearTimeout(tlsTimeoutId);
357
+ reject(new Error(`TLS handshake failed: ${err.message}`));
358
+ });
359
+ } else {
360
+ resolve(socket);
361
+ }
362
+ }).catch((err) => {
363
+ reject(new Error(`SOCKS proxy connection failed: ${err.message}`));
364
+ });
365
+ return;
366
+ }
367
+ const proxyHost = proxyUrl.hostname;
368
+ const proxyPort = parseInt(proxyUrl.port || (proxyUrl.protocol === "https:" ? "443" : "80"), 10);
369
+ let proxySocket;
370
+ const connectToProxy = () => {
371
+ if (proxyUrl.protocol === "https:") {
372
+ proxySocket = tls.connect({
373
+ host: proxyHost,
374
+ port: proxyPort,
375
+ rejectUnauthorized: rejectUnauthorized !== false
376
+ });
377
+ } else {
378
+ proxySocket = net.connect({
379
+ host: proxyHost,
380
+ port: proxyPort
381
+ });
382
+ }
383
+ let settled = false;
384
+ const timeoutId = timeout ? setTimeout(() => {
385
+ if (!settled) {
386
+ settled = true;
387
+ proxySocket.destroy();
388
+ reject(new Error(`Proxy connection timeout after ${timeout}ms`));
389
+ }
390
+ }, timeout) : null;
391
+ proxySocket.on("error", (err) => {
392
+ if (!settled) {
393
+ settled = true;
394
+ if (timeoutId)
395
+ clearTimeout(timeoutId);
396
+ reject(new Error(`Proxy connection error: ${err.message}`));
397
+ }
398
+ });
399
+ proxySocket.on("connect", () => {
400
+ const connectRequest = [
401
+ `CONNECT ${targetHost}:${targetPort} HTTP/1.1`,
402
+ `Host: ${targetHost}:${targetPort}`,
403
+ proxyAuth ? `Proxy-Authorization: Basic ${proxyAuth}` : "",
404
+ "",
405
+ ""
406
+ ].filter(Boolean).join(`\r
407
+ `);
408
+ proxySocket.write(connectRequest);
409
+ });
410
+ let responseBuffer = "";
411
+ proxySocket.on("data", function onData(data) {
412
+ if (settled)
413
+ return;
414
+ responseBuffer += data.toString();
415
+ const headerEnd = responseBuffer.indexOf(`\r
416
+ \r
417
+ `);
418
+ if (headerEnd !== -1) {
419
+ settled = true;
420
+ if (timeoutId)
421
+ clearTimeout(timeoutId);
422
+ proxySocket.removeListener("data", onData);
423
+ const statusLine = responseBuffer.split(`\r
424
+ `)[0];
425
+ const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d{3})/);
426
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 0;
427
+ if (statusCode === 200) {
428
+ if (url.protocol === "https:") {
429
+ const tlsSocket = tls.connect({
430
+ socket: proxySocket,
431
+ host: targetHost,
432
+ servername: targetHost,
433
+ rejectUnauthorized: rejectUnauthorized !== false,
434
+ ALPNProtocols: ["h2", "http/1.1"]
435
+ });
436
+ const tlsTimeoutId = timeout ? setTimeout(() => {
437
+ tlsSocket.destroy();
438
+ reject(new Error(`TLS handshake timeout after ${timeout}ms`));
439
+ }, timeout) : null;
440
+ tlsSocket.on("secureConnect", () => {
441
+ if (tlsTimeoutId)
442
+ clearTimeout(tlsTimeoutId);
443
+ const alpn = tlsSocket.alpnProtocol;
444
+ if (alpn && alpn !== "h2") {
445
+ tlsSocket.destroy();
446
+ reject(new Error(`Server does not support HTTP/2 (negotiated: ${alpn})`));
447
+ return;
448
+ }
449
+ resolve(tlsSocket);
450
+ });
451
+ tlsSocket.on("error", (err) => {
452
+ if (tlsTimeoutId)
453
+ clearTimeout(tlsTimeoutId);
454
+ reject(new Error(`TLS handshake failed: ${err.message}`));
455
+ });
456
+ } else {
457
+ resolve(proxySocket);
458
+ }
459
+ } else {
460
+ proxySocket.destroy();
461
+ reject(new Error(`Proxy CONNECT failed with status ${statusCode}: ${statusLine}`));
462
+ }
463
+ }
464
+ });
465
+ };
466
+ connectToProxy();
467
+ });
468
+ }
469
+ releaseSession(url, proxy) {
470
+ const key = this.getSessionKey(url, undefined, proxy);
284
471
  const entry = this.sessions.get(key);
285
472
  if (entry) {
286
473
  entry.refCount = Math.max(0, entry.refCount - 1);
@@ -293,8 +480,8 @@ class Http2SessionPool {
293
480
  }
294
481
  }
295
482
  }
296
- closeSession(url) {
297
- const key = this.getSessionKey(url);
483
+ closeSession(url, proxy) {
484
+ const key = this.getSessionKey(url, undefined, proxy);
298
485
  const entry = this.sessions.get(key);
299
486
  if (entry) {
300
487
  entry.session.close();
@@ -1008,10 +1195,10 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1008
1195
  const forceNewSession = requestCount > 0;
1009
1196
  let session;
1010
1197
  if (config.debug) {
1011
- console.log(`[Rezo Debug] HTTP/2: Acquiring session for ${url.host}${forceNewSession ? " (forcing new for redirect)" : ""}...`);
1198
+ console.log(`[Rezo Debug] HTTP/2: Acquiring session for ${url.host}${forceNewSession ? " (forcing new for redirect)" : ""}${fetchOptions.proxy ? " (via proxy)" : ""}...`);
1012
1199
  }
1013
1200
  try {
1014
- session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined, forceNewSession);
1201
+ session = await (sessionPool || Http2SessionPool.getInstance()).getSession(url, sessionOptions, config.timeout !== null ? config.timeout : undefined, forceNewSession, fetchOptions.proxy);
1015
1202
  if (config.debug) {
1016
1203
  console.log(`[Rezo Debug] HTTP/2: Session acquired successfully`);
1017
1204
  }
@@ -1240,7 +1427,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1240
1427
  config.transfer.requestSize = Buffer.byteLength(JSON.stringify(body), "utf8");
1241
1428
  }
1242
1429
  }
1243
- (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
1430
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
1244
1431
  if (isRedirect) {
1245
1432
  _stats.statusOnNext = "redirect";
1246
1433
  const partialResponse = {
@@ -1417,7 +1604,7 @@ async function executeHttp2Stream(config, fetchOptions, requestCount, timing, _s
1417
1604
  if (config.debug) {
1418
1605
  console.log(`[Rezo Debug] HTTP/2: Error in 'end' handler:`, endError.message);
1419
1606
  }
1420
- (sessionPool || Http2SessionPool.getInstance()).releaseSession(url);
1607
+ (sessionPool || Http2SessionPool.getInstance()).releaseSession(url, fetchOptions.proxy);
1421
1608
  const error = buildSmartError(config, fetchOptions, endError);
1422
1609
  _stats.statusOnNext = "error";
1423
1610
  resolve(error);
@@ -1,6 +1,6 @@
1
- const _mod_8y5us8 = require('./picker.cjs');
2
- exports.detectRuntime = _mod_8y5us8.detectRuntime;
3
- exports.getAdapterCapabilities = _mod_8y5us8.getAdapterCapabilities;
4
- exports.buildAdapterContext = _mod_8y5us8.buildAdapterContext;
5
- exports.getAvailableAdapters = _mod_8y5us8.getAvailableAdapters;
6
- exports.selectAdapter = _mod_8y5us8.selectAdapter;;
1
+ const _mod_63iyz4 = require('./picker.cjs');
2
+ exports.detectRuntime = _mod_63iyz4.detectRuntime;
3
+ exports.getAdapterCapabilities = _mod_63iyz4.getAdapterCapabilities;
4
+ exports.buildAdapterContext = _mod_63iyz4.buildAdapterContext;
5
+ exports.getAvailableAdapters = _mod_63iyz4.getAvailableAdapters;
6
+ exports.selectAdapter = _mod_63iyz4.selectAdapter;;
@@ -45,7 +45,13 @@ async function createDatabase(dbPath) {
45
45
  const { DatabaseSync } = await import("node:sqlite");
46
46
  const db = new DatabaseSync(dbPath);
47
47
  return {
48
- run: (sql, ...params) => db.exec(sql),
48
+ run: (sql, ...params) => {
49
+ if (params.length === 0) {
50
+ db.exec(sql);
51
+ } else {
52
+ db.prepare(sql).run(...params);
53
+ }
54
+ },
49
55
  get: (sql, ...params) => {
50
56
  const stmt = db.prepare(sql);
51
57
  return stmt.get(...params);
@@ -45,7 +45,13 @@ async function createDatabase(dbPath) {
45
45
  const { DatabaseSync } = await import("node:sqlite");
46
46
  const db = new DatabaseSync(dbPath);
47
47
  return {
48
- run: (sql, ...params) => db.exec(sql),
48
+ run: (sql, ...params) => {
49
+ if (params.length === 0) {
50
+ db.exec(sql);
51
+ } else {
52
+ db.prepare(sql).run(...params);
53
+ }
54
+ },
49
55
  get: (sql, ...params) => {
50
56
  const stmt = db.prepare(sql);
51
57
  return stmt.get(...params);
@@ -1,13 +1,15 @@
1
- const _mod_q88avp = require('./lru-cache.cjs');
2
- exports.LRUCache = _mod_q88avp.LRUCache;;
3
- const _mod_ycn5kr = require('./dns-cache.cjs');
4
- exports.DNSCache = _mod_ycn5kr.DNSCache;
5
- exports.getGlobalDNSCache = _mod_ycn5kr.getGlobalDNSCache;
6
- exports.resetGlobalDNSCache = _mod_ycn5kr.resetGlobalDNSCache;;
7
- const _mod_ca0ngi = require('./response-cache.cjs');
8
- exports.ResponseCache = _mod_ca0ngi.ResponseCache;
9
- exports.normalizeResponseCacheConfig = _mod_ca0ngi.normalizeResponseCacheConfig;;
10
- const _mod_pmo9e4 = require('./file-cacher.cjs');
11
- exports.FileCacher = _mod_pmo9e4.FileCacher;;
12
- const _mod_h6jprd = require('./url-store.cjs');
13
- exports.UrlStore = _mod_h6jprd.UrlStore;;
1
+ const _mod_6dj0p0 = require('./lru-cache.cjs');
2
+ exports.LRUCache = _mod_6dj0p0.LRUCache;;
3
+ const _mod_9z4pm1 = require('./dns-cache.cjs');
4
+ exports.DNSCache = _mod_9z4pm1.DNSCache;
5
+ exports.getGlobalDNSCache = _mod_9z4pm1.getGlobalDNSCache;
6
+ exports.resetGlobalDNSCache = _mod_9z4pm1.resetGlobalDNSCache;;
7
+ const _mod_5ylf2b = require('./response-cache.cjs');
8
+ exports.ResponseCache = _mod_5ylf2b.ResponseCache;
9
+ exports.normalizeResponseCacheConfig = _mod_5ylf2b.normalizeResponseCacheConfig;;
10
+ const _mod_fk65c2 = require('./file-cacher.cjs');
11
+ exports.FileCacher = _mod_fk65c2.FileCacher;;
12
+ const _mod_7tzsb5 = require('./url-store.cjs');
13
+ exports.UrlStore = _mod_7tzsb5.UrlStore;;
14
+ const _mod_04c8wb = require('./navigation-history.cjs');
15
+ exports.NavigationHistory = _mod_04c8wb.NavigationHistory;;
@@ -3,3 +3,4 @@ export { DNSCache, getGlobalDNSCache, resetGlobalDNSCache } from './dns-cache.js
3
3
  export { ResponseCache, normalizeResponseCacheConfig } from './response-cache.js';
4
4
  export { FileCacher } from './file-cacher.js';
5
5
  export { UrlStore } from './url-store.js';
6
+ export { NavigationHistory } from './navigation-history.js';
@@ -0,0 +1,298 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { createHash } = require("node:crypto");
4
+ function detectRuntime() {
5
+ if (typeof globalThis.Bun !== "undefined")
6
+ return "bun";
7
+ if (typeof globalThis.Deno !== "undefined")
8
+ return "deno";
9
+ return "node";
10
+ }
11
+ async function createDatabase(dbPath) {
12
+ const runtime = detectRuntime();
13
+ if (runtime === "bun") {
14
+ const { Database } = await import("bun:sqlite");
15
+ const db = new Database(dbPath);
16
+ return {
17
+ run: (sql, ...params) => db.run(sql, ...params),
18
+ get: (sql, ...params) => db.query(sql).get(...params),
19
+ all: (sql, ...params) => db.query(sql).all(...params),
20
+ close: () => db.close()
21
+ };
22
+ }
23
+ if (runtime === "deno") {
24
+ try {
25
+ const { Database } = await import("node:sqlite");
26
+ const db = new Database(dbPath);
27
+ return {
28
+ run: (sql, ...params) => db.exec(sql, params),
29
+ get: (sql, ...params) => {
30
+ const stmt = db.prepare(sql);
31
+ return stmt.get(...params);
32
+ },
33
+ all: (sql, ...params) => {
34
+ const stmt = db.prepare(sql);
35
+ return stmt.all(...params);
36
+ },
37
+ close: () => db.close()
38
+ };
39
+ } catch {
40
+ throw new Error("Deno SQLite support requires Node.js compatibility mode");
41
+ }
42
+ }
43
+ const { DatabaseSync } = await import("node:sqlite");
44
+ const db = new DatabaseSync(dbPath);
45
+ return {
46
+ run: (sql, ...params) => {
47
+ const stmt = db.prepare(sql);
48
+ stmt.run(...params);
49
+ },
50
+ get: (sql, ...params) => {
51
+ const stmt = db.prepare(sql);
52
+ return stmt.get(...params);
53
+ },
54
+ all: (sql, ...params) => {
55
+ const stmt = db.prepare(sql);
56
+ return stmt.all(...params);
57
+ },
58
+ close: () => db.close()
59
+ };
60
+ }
61
+
62
+ class NavigationHistory {
63
+ db = null;
64
+ options;
65
+ storeDir;
66
+ dbPath;
67
+ closed = false;
68
+ initPromise = null;
69
+ constructor(options = {}) {
70
+ this.options = {
71
+ storeDir: options.storeDir || "./navigation-history",
72
+ dbFileName: options.dbFileName || "navigation.db",
73
+ hashUrls: options.hashUrls ?? false
74
+ };
75
+ this.storeDir = path.resolve(this.options.storeDir);
76
+ this.dbPath = path.join(this.storeDir, this.options.dbFileName);
77
+ if (!fs.existsSync(this.storeDir)) {
78
+ fs.mkdirSync(this.storeDir, { recursive: true });
79
+ }
80
+ }
81
+ static async create(options = {}) {
82
+ const store = new NavigationHistory(options);
83
+ await store.initialize();
84
+ return store;
85
+ }
86
+ async initialize() {
87
+ if (this.initPromise)
88
+ return this.initPromise;
89
+ this.initPromise = (async () => {
90
+ this.db = await createDatabase(this.dbPath);
91
+ this.db.run(`
92
+ CREATE TABLE IF NOT EXISTS sessions (
93
+ sessionId TEXT PRIMARY KEY,
94
+ baseUrl TEXT NOT NULL,
95
+ startedAt INTEGER NOT NULL,
96
+ lastActivityAt INTEGER NOT NULL,
97
+ status TEXT DEFAULT 'running',
98
+ urlsVisited INTEGER DEFAULT 0,
99
+ urlsQueued INTEGER DEFAULT 0,
100
+ urlsFailed INTEGER DEFAULT 0,
101
+ metadata TEXT
102
+ )
103
+ `);
104
+ this.db.run(`
105
+ CREATE TABLE IF NOT EXISTS queue (
106
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
107
+ sessionId TEXT NOT NULL,
108
+ urlKey TEXT NOT NULL,
109
+ originalUrl TEXT NOT NULL,
110
+ method TEXT DEFAULT 'GET',
111
+ priority INTEGER DEFAULT 0,
112
+ body TEXT,
113
+ headers TEXT,
114
+ metadata TEXT,
115
+ addedAt INTEGER NOT NULL,
116
+ UNIQUE(sessionId, urlKey)
117
+ )
118
+ `);
119
+ this.db.run(`
120
+ CREATE TABLE IF NOT EXISTS visited (
121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
122
+ sessionId TEXT NOT NULL,
123
+ urlKey TEXT NOT NULL,
124
+ originalUrl TEXT NOT NULL,
125
+ status INTEGER,
126
+ visitedAt INTEGER NOT NULL,
127
+ finalUrl TEXT,
128
+ contentType TEXT,
129
+ errorMessage TEXT,
130
+ UNIQUE(sessionId, urlKey)
131
+ )
132
+ `);
133
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_queue_session ON queue(sessionId)");
134
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_queue_priority ON queue(sessionId, priority DESC)");
135
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_visited_session ON visited(sessionId)");
136
+ this.db.run("CREATE INDEX IF NOT EXISTS idx_sessions_status ON sessions(status)");
137
+ })();
138
+ return this.initPromise;
139
+ }
140
+ getUrlKey(url) {
141
+ if (this.options.hashUrls) {
142
+ return createHash("sha256").update(url).digest("hex");
143
+ }
144
+ return url;
145
+ }
146
+ async createSession(sessionId, baseUrl, metadata) {
147
+ if (this.closed || !this.db)
148
+ throw new Error("NavigationHistory is closed");
149
+ const now = Date.now();
150
+ const session = {
151
+ sessionId,
152
+ baseUrl,
153
+ startedAt: now,
154
+ lastActivityAt: now,
155
+ status: "running",
156
+ urlsVisited: 0,
157
+ urlsQueued: 0,
158
+ urlsFailed: 0,
159
+ metadata: metadata ? JSON.stringify(metadata) : undefined
160
+ };
161
+ this.db.run(`INSERT OR REPLACE INTO sessions (sessionId, baseUrl, startedAt, lastActivityAt, status, urlsVisited, urlsQueued, urlsFailed, metadata)
162
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, sessionId, baseUrl, now, now, "running", 0, 0, 0, session.metadata ?? null);
163
+ return session;
164
+ }
165
+ async getSession(sessionId) {
166
+ if (this.closed || !this.db)
167
+ throw new Error("NavigationHistory is closed");
168
+ return this.db.get("SELECT * FROM sessions WHERE sessionId = ?", sessionId);
169
+ }
170
+ async updateSessionStatus(sessionId, status) {
171
+ if (this.closed || !this.db)
172
+ throw new Error("NavigationHistory is closed");
173
+ this.db.run("UPDATE sessions SET status = ?, lastActivityAt = ? WHERE sessionId = ?", status, Date.now(), sessionId);
174
+ }
175
+ async updateSessionStats(sessionId, stats) {
176
+ if (this.closed || !this.db)
177
+ throw new Error("NavigationHistory is closed");
178
+ const updates = ["lastActivityAt = ?"];
179
+ const params = [Date.now()];
180
+ if (stats.urlsVisited !== undefined) {
181
+ updates.push("urlsVisited = ?");
182
+ params.push(stats.urlsVisited);
183
+ }
184
+ if (stats.urlsQueued !== undefined) {
185
+ updates.push("urlsQueued = ?");
186
+ params.push(stats.urlsQueued);
187
+ }
188
+ if (stats.urlsFailed !== undefined) {
189
+ updates.push("urlsFailed = ?");
190
+ params.push(stats.urlsFailed);
191
+ }
192
+ params.push(sessionId);
193
+ this.db.run(`UPDATE sessions SET ${updates.join(", ")} WHERE sessionId = ?`, ...params);
194
+ }
195
+ async addToQueue(sessionId, url, options = {}) {
196
+ if (this.closed || !this.db)
197
+ throw new Error("NavigationHistory is closed");
198
+ const urlKey = this.getUrlKey(url);
199
+ const existing = this.db.get("SELECT id FROM queue WHERE sessionId = ? AND urlKey = ?", sessionId, urlKey);
200
+ if (existing)
201
+ return false;
202
+ const isVisited = this.db.get("SELECT id FROM visited WHERE sessionId = ? AND urlKey = ?", sessionId, urlKey);
203
+ if (isVisited)
204
+ return false;
205
+ this.db.run(`INSERT INTO queue (sessionId, urlKey, originalUrl, method, priority, body, headers, metadata, addedAt)
206
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, sessionId, urlKey, url, options.method || "GET", options.priority || 0, options.body ? JSON.stringify(options.body) : null, options.headers ? JSON.stringify(options.headers) : null, options.metadata ? JSON.stringify(options.metadata) : null, Date.now());
207
+ return true;
208
+ }
209
+ async getNextFromQueue(sessionId) {
210
+ if (this.closed || !this.db)
211
+ throw new Error("NavigationHistory is closed");
212
+ const item = this.db.get("SELECT originalUrl as url, method, priority, body, headers, metadata, addedAt FROM queue WHERE sessionId = ? ORDER BY priority DESC, addedAt ASC LIMIT 1", sessionId);
213
+ return item;
214
+ }
215
+ async removeFromQueue(sessionId, url) {
216
+ if (this.closed || !this.db)
217
+ throw new Error("NavigationHistory is closed");
218
+ const urlKey = this.getUrlKey(url);
219
+ this.db.run("DELETE FROM queue WHERE sessionId = ? AND urlKey = ?", sessionId, urlKey);
220
+ return true;
221
+ }
222
+ async getQueueSize(sessionId) {
223
+ if (this.closed || !this.db)
224
+ throw new Error("NavigationHistory is closed");
225
+ const result = this.db.get("SELECT COUNT(*) as count FROM queue WHERE sessionId = ?", sessionId);
226
+ return result?.count || 0;
227
+ }
228
+ async markVisited(sessionId, url, result = {}) {
229
+ if (this.closed || !this.db)
230
+ throw new Error("NavigationHistory is closed");
231
+ const urlKey = this.getUrlKey(url);
232
+ this.db.run("DELETE FROM queue WHERE sessionId = ? AND urlKey = ?", sessionId, urlKey);
233
+ this.db.run(`INSERT OR REPLACE INTO visited (sessionId, urlKey, originalUrl, status, visitedAt, finalUrl, contentType, errorMessage)
234
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, sessionId, urlKey, url, result.status || 0, Date.now(), result.finalUrl ?? null, result.contentType ?? null, result.errorMessage ?? null);
235
+ }
236
+ async isVisited(sessionId, url) {
237
+ if (this.closed || !this.db)
238
+ throw new Error("NavigationHistory is closed");
239
+ const urlKey = this.getUrlKey(url);
240
+ const result = this.db.get("SELECT id FROM visited WHERE sessionId = ? AND urlKey = ?", sessionId, urlKey);
241
+ return !!result;
242
+ }
243
+ async getVisitedCount(sessionId) {
244
+ if (this.closed || !this.db)
245
+ throw new Error("NavigationHistory is closed");
246
+ const result = this.db.get("SELECT COUNT(*) as count FROM visited WHERE sessionId = ?", sessionId);
247
+ return result?.count || 0;
248
+ }
249
+ async getFailedUrls(sessionId) {
250
+ if (this.closed || !this.db)
251
+ throw new Error("NavigationHistory is closed");
252
+ return this.db.all("SELECT url, status, visitedAt, finalUrl, contentType, errorMessage FROM visited WHERE sessionId = ? AND (status >= 400 OR errorMessage IS NOT NULL)", sessionId);
253
+ }
254
+ async getAllQueuedUrls(sessionId) {
255
+ if (this.closed || !this.db)
256
+ throw new Error("NavigationHistory is closed");
257
+ return this.db.all("SELECT originalUrl as url, method, priority, body, headers, metadata, addedAt FROM queue WHERE sessionId = ? ORDER BY priority DESC, addedAt ASC", sessionId);
258
+ }
259
+ async clearQueue(sessionId) {
260
+ if (this.closed || !this.db)
261
+ throw new Error("NavigationHistory is closed");
262
+ this.db.run("DELETE FROM queue WHERE sessionId = ?", sessionId);
263
+ }
264
+ async clearVisited(sessionId) {
265
+ if (this.closed || !this.db)
266
+ throw new Error("NavigationHistory is closed");
267
+ this.db.run("DELETE FROM visited WHERE sessionId = ?", sessionId);
268
+ }
269
+ async deleteSession(sessionId) {
270
+ if (this.closed || !this.db)
271
+ throw new Error("NavigationHistory is closed");
272
+ this.db.run("DELETE FROM queue WHERE sessionId = ?", sessionId);
273
+ this.db.run("DELETE FROM visited WHERE sessionId = ?", sessionId);
274
+ this.db.run("DELETE FROM sessions WHERE sessionId = ?", sessionId);
275
+ }
276
+ async getResumableSessions() {
277
+ if (this.closed || !this.db)
278
+ throw new Error("NavigationHistory is closed");
279
+ return this.db.all("SELECT * FROM sessions WHERE status IN ('running', 'paused') ORDER BY lastActivityAt DESC");
280
+ }
281
+ async close() {
282
+ if (this.closed)
283
+ return;
284
+ this.closed = true;
285
+ if (this.db) {
286
+ this.db.close();
287
+ this.db = null;
288
+ }
289
+ }
290
+ get isClosed() {
291
+ return this.closed;
292
+ }
293
+ get databasePath() {
294
+ return this.dbPath;
295
+ }
296
+ }
297
+
298
+ exports.NavigationHistory = NavigationHistory;