From c74f713b2dc081c94858cc7bfaee3e952c40a536 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 26 Nov 2025 23:20:52 +0000
Subject: [PATCH 1/3] Initial plan
From dd38c5b79e036d11932cd8820e91e8d77136fdb6 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 26 Nov 2025 23:31:54 +0000
Subject: [PATCH 2/3] Optimize backend proxy for speed and stability, improve
experimental features
Co-authored-by: sriail <225764385+sriail@users.noreply.github.com>
---
public/sw.js | 151 ++++++++++++----
server/index.ts | 53 +++++-
src/utils/captcha-handler.ts | 157 ++++++++++++-----
src/utils/experimental.ts | 325 +++++++++++++++++++++++++++++++++++
src/utils/proxy.ts | 202 ++++++++++++++++++----
src/utils/storage.ts | 51 +++++-
6 files changed, 816 insertions(+), 123 deletions(-)
create mode 100644 src/utils/experimental.ts
diff --git a/public/sw.js b/public/sw.js
index 3266d21..d350547 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -17,9 +17,17 @@ const sj = new ScramjetServiceWorker({
}
});
+// Cache for routing decisions to avoid redundant checks
+const routeCache = new Map();
+const ROUTE_CACHE_MAX_SIZE = 500;
+const ROUTE_CACHE_TTL = 30000; // 30 seconds
+
+// Cache for domain checks to avoid redundant string operations
+const domainCheckCache = new Map();
+
// Enhanced CAPTCHA and Cloudflare verification support
-// List of CAPTCHA and verification domains that need special handling
-const CAPTCHA_DOMAINS = [
+// Use Set for O(1) lookup instead of Array with some()
+const CAPTCHA_DOMAINS = new Set([
"google.com/recaptcha",
"www.google.com/recaptcha",
"recaptcha.net",
@@ -30,10 +38,10 @@ const CAPTCHA_DOMAINS = [
"challenges.cloudflare.com",
"cloudflare.com/cdn-cgi/challenge",
"turnstile.cloudflare.com"
-];
+]);
// Domains that use heavy cookies and complex browser services
-const HEAVY_COOKIE_DOMAINS = [
+const HEAVY_COOKIE_DOMAINS = new Set([
"amazon.com",
"ebay.com",
"walmart.com",
@@ -47,20 +55,61 @@ const HEAVY_COOKIE_DOMAINS = [
"apple.com",
"netflix.com",
"spotify.com"
-];
+]);
-// Helper function to check if URL is CAPTCHA-related
+// Optimized helper function to check if URL is CAPTCHA-related
function isCaptchaRequest(url) {
+ const cacheKey = "captcha:" + url;
+ if (domainCheckCache.has(cacheKey)) {
+ return domainCheckCache.get(cacheKey);
+ }
+
const urlStr = url.toString().toLowerCase();
- return CAPTCHA_DOMAINS.some((domain) => urlStr.includes(domain));
+ let result = false;
+ for (const domain of CAPTCHA_DOMAINS) {
+ if (urlStr.includes(domain)) {
+ result = true;
+ break;
+ }
+ }
+
+ // Cache with size limit
+ if (domainCheckCache.size >= 1000) {
+ const firstKey = domainCheckCache.keys().next().value;
+ domainCheckCache.delete(firstKey);
+ }
+ domainCheckCache.set(cacheKey, result);
+ return result;
}
-// Helper function to check if URL is from a site with heavy cookies
+// Optimized helper function to check if URL is from a site with heavy cookies
function isHeavyCookieSite(url) {
+ const cacheKey = "heavy:" + url;
+ if (domainCheckCache.has(cacheKey)) {
+ return domainCheckCache.get(cacheKey);
+ }
+
const urlStr = url.toString().toLowerCase();
- return HEAVY_COOKIE_DOMAINS.some((domain) => urlStr.includes(domain));
+ let result = false;
+ for (const domain of HEAVY_COOKIE_DOMAINS) {
+ if (urlStr.includes(domain)) {
+ result = true;
+ break;
+ }
+ }
+
+ // Cache with size limit
+ if (domainCheckCache.size >= 1000) {
+ const firstKey = domainCheckCache.keys().next().value;
+ domainCheckCache.delete(firstKey);
+ }
+ domainCheckCache.set(cacheKey, result);
+ return result;
}
+// Reusable headers for CAPTCHA requests
+const CAPTCHA_HEADERS_TEMPLATE = { Accept: "*/*" };
+
// Helper function to ensure proper CAPTCHA handling
function enhanceCaptchaRequest(request) {
// Clone the request to ensure all headers and properties are preserved
@@ -68,7 +117,7 @@ function enhanceCaptchaRequest(request) {
// Ensure proper headers for CAPTCHA requests
if (!headers.has("Accept")) {
- headers.set("Accept", "*/*");
+ headers.set("Accept", CAPTCHA_HEADERS_TEMPLATE.Accept);
}
// Preserve credentials for CAPTCHA cookies
@@ -91,29 +140,63 @@ function enhanceHeavyCookieRequest(request) {
});
}
+// Cache sj config loading status
+let sjConfigLoaded = false;
+
self.addEventListener("fetch", function (event) {
event.respondWith(
(async () => {
try {
- await sj.loadConfig();
+ // Only load config once per SW lifecycle
+ if (!sjConfigLoaded) {
+ await sj.loadConfig();
+ sjConfigLoaded = true;
+ }
const url = event.request.url;
- const isCaptcha = isCaptchaRequest(url);
- const isHeavyCookie = isHeavyCookieSite(url);
- // Safely check if this is a proxied request using optional chaining
- const uvPrefix =
- (typeof __uv$config !== "undefined" && __uv$config?.prefix) || null;
- const isUvRequest = uvPrefix ? url.startsWith(location.origin + uvPrefix) : false;
- const isSjRequest = sj.route(event);
- const isProxiedRequest = isUvRequest || isSjRequest;
+ // Check route cache for faster routing
+ let routeInfo = routeCache.get(url);
+ const now = Date.now();
+
+ if (!routeInfo || now - routeInfo.timestamp > ROUTE_CACHE_TTL) {
+ // Safely check if this is a proxied request using optional chaining
+ const uvPrefix =
+ (typeof __uv$config !== "undefined" && __uv$config?.prefix) || null;
+ const isUvRequest = uvPrefix
+ ? url.startsWith(location.origin + uvPrefix)
+ : false;
+ const isSjRequest = sj.route(event);
+
+ routeInfo = {
+ isUvRequest,
+ isSjRequest,
+ isProxiedRequest: isUvRequest || isSjRequest,
+ timestamp: now
+ };
- // Enhanced handling for CAPTCHA and heavy cookie requests
+ // Cache with size limit
+ if (routeCache.size >= ROUTE_CACHE_MAX_SIZE) {
+ const firstKey = routeCache.keys().next().value;
+ routeCache.delete(firstKey);
+ }
+ routeCache.set(url, routeInfo);
+ }
+
+ const { isUvRequest, isSjRequest, isProxiedRequest } = routeInfo;
+
+ // Only check CAPTCHA and heavy cookie for non-proxied requests
let request = event.request;
- if (isCaptcha) {
- request = enhanceCaptchaRequest(event.request);
- } else if (isHeavyCookie) {
- request = enhanceHeavyCookieRequest(event.request);
+ if (!isProxiedRequest) {
+ const isCaptcha = isCaptchaRequest(url);
+ if (isCaptcha) {
+ request = enhanceCaptchaRequest(event.request);
+ } else {
+ const isHeavyCookie = isHeavyCookieSite(url);
+ if (isHeavyCookie) {
+ request = enhanceHeavyCookieRequest(event.request);
+ }
+ }
}
let response;
@@ -205,6 +288,11 @@ const INTERCEPTOR_SCRIPT = `
`;
+// Precompiled regex patterns for better performance
+const HEAD_TAG_REGEX = /
]*)?>/i;
+const BODY_TAG_REGEX = /]*)?>/i;
+const HTML_TAG_REGEX = /]*)?>/i;
+
// Helper function to inject script into HTML responses
async function injectInterceptorScript(response) {
const contentType = response.headers.get("content-type") || "";
@@ -226,26 +314,25 @@ async function injectInterceptorScript(response) {
});
}
- // Inject the script just after opening tags
- // Handle both normal opening tags and self-closing tags
+ // Inject the script just after opening tags using precompiled regex
let modifiedHtml = text;
let injected = false;
// Try to inject after tag (normal or self-closing)
- if (!injected && /]*)?>/i.test(text)) {
- modifiedHtml = text.replace(/]*)?>/i, (match) => match + INTERCEPTOR_SCRIPT);
+ if (!injected && HEAD_TAG_REGEX.test(text)) {
+ modifiedHtml = text.replace(HEAD_TAG_REGEX, (match) => match + INTERCEPTOR_SCRIPT);
injected = true;
}
// Fallback: inject after tag (normal or self-closing)
- if (!injected && /]*)?>/i.test(text)) {
- modifiedHtml = text.replace(/]*)?>/i, (match) => match + INTERCEPTOR_SCRIPT);
+ if (!injected && BODY_TAG_REGEX.test(text)) {
+ modifiedHtml = text.replace(BODY_TAG_REGEX, (match) => match + INTERCEPTOR_SCRIPT);
injected = true;
}
// Last resort: inject after tag (normal or self-closing)
- if (!injected && /]*)?>/i.test(text)) {
- modifiedHtml = text.replace(/]*)?>/i, (match) => match + INTERCEPTOR_SCRIPT);
+ if (!injected && HTML_TAG_REGEX.test(text)) {
+ modifiedHtml = text.replace(HTML_TAG_REGEX, (match) => match + INTERCEPTOR_SCRIPT);
injected = true;
}
diff --git a/server/index.ts b/server/index.ts
index b23f01d..a66d97d 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -16,6 +16,21 @@ import { handler as astroHandler } from "../dist/server/entry.mjs";
import { createServer } from "node:http";
import { Socket } from "node:net";
+// Cache for routing decisions to reduce repeated checks
+const routingCache = new Map();
+const ROUTING_CACHE_MAX_SIZE = 1000;
+const ROUTING_CACHE_TTL = 60000; // 1 minute
+
+// Helper to manage routing cache with TTL and size limit
+const cacheRoutingDecision = (url: string, decision: "bare" | "wisp" | "static"): void => {
+ if (routingCache.size >= ROUTING_CACHE_MAX_SIZE) {
+ // Remove oldest entries (first 100)
+ const keysToDelete = Array.from(routingCache.keys()).slice(0, 100);
+ keysToDelete.forEach((key) => routingCache.delete(key));
+ }
+ routingCache.set(url, decision);
+};
+
const bareServer = createBareServer("/bare/", {
connectionLimiter: {
// Optimized for sites with heavy cookies and complex browser services
@@ -35,13 +50,26 @@ const serverFactory: FastifyServerFactory = (
keepAlive: true,
keepAliveTimeout: 65000, // 65 seconds
// Increase timeout for long-running requests
- requestTimeout: 120000 // 120 seconds
+ requestTimeout: 120000, // 120 seconds
+ // Enable high watermark for better throughput
+ highWaterMark: 65536 // 64KB buffer
});
server
.on("request", (req, res) => {
try {
+ const url = req.url || "";
+
+ // Check routing cache first for faster routing decisions
+ const cachedDecision = routingCache.get(url);
+ if (cachedDecision === "bare") {
+ bareServer.routeRequest(req, res);
+ return;
+ }
+
+ // Make routing decision
if (bareServer.shouldRoute(req)) {
+ cacheRoutingDecision(url, "bare");
bareServer.routeRequest(req, res);
} else {
handler(req, res);
@@ -56,10 +84,16 @@ const serverFactory: FastifyServerFactory = (
})
.on("upgrade", (req, socket, head) => {
try {
+ const url = req.url || "";
+ const isWisp = url.endsWith("/wisp/") || url.endsWith("/adblock/");
+
if (bareServer.shouldRoute(req)) {
bareServer.routeUpgrade(req, socket as Socket, head);
- } else if (req.url?.endsWith("/wisp/") || req.url?.endsWith("/adblock/")) {
- console.log("WebSocket upgrade:", req.url);
+ } else if (isWisp) {
+ // Only log in development mode to reduce overhead
+ if (process.env.NODE_ENV === "development") {
+ console.log("WebSocket upgrade:", url);
+ }
wisp.routeRequest(req, socket as Socket, head);
}
} catch (error) {
@@ -71,7 +105,10 @@ const serverFactory: FastifyServerFactory = (
console.error("Server error:", error);
})
.on("clientError", (error, socket) => {
- console.error("Client error:", error);
+ // Only log in development to reduce console spam
+ if (process.env.NODE_ENV === "development") {
+ console.error("Client error:", error);
+ }
if (!socket.destroyed) {
socket.end("HTTP/1.1 400 Bad Request\r\n\r\n");
}
@@ -95,7 +132,13 @@ const app = Fastify({
});
await app.register(fastifyStatic, {
- root: fileURLToPath(new URL("../dist/client", import.meta.url))
+ root: fileURLToPath(new URL("../dist/client", import.meta.url)),
+ // Enable caching headers for static resources
+ cacheControl: true,
+ maxAge: 86400000, // 1 day for static assets
+ immutable: true,
+ // Enable precompressed files if available
+ preCompressed: true
});
await app.register(fastifyMiddie);
diff --git a/src/utils/captcha-handler.ts b/src/utils/captcha-handler.ts
index 9ddf6ac..e45d8a7 100644
--- a/src/utils/captcha-handler.ts
+++ b/src/utils/captcha-handler.ts
@@ -6,21 +6,21 @@
*/
/**
- * List of CAPTCHA and verification-related domains
+ * Use Sets for O(1) lookup instead of Array.some()
*/
-const CAPTCHA_DOMAINS = [
+const CAPTCHA_DOMAINS = new Set([
"google.com",
"recaptcha.net",
"gstatic.com",
"hcaptcha.com",
"cloudflare.com",
"challenges.cloudflare.com"
-];
+]);
/**
- * List of domains known to use heavy cookies and complex browser services
+ * Set of domains known to use heavy cookies and complex browser services
*/
-const HEAVY_COOKIE_DOMAINS = [
+const HEAVY_COOKIE_DOMAINS = new Set([
"amazon.com",
"ebay.com",
"walmart.com",
@@ -34,7 +34,47 @@ const HEAVY_COOKIE_DOMAINS = [
"apple.com",
"netflix.com",
"spotify.com"
-];
+]);
+
+// Cache for domain checks to avoid redundant operations
+const domainCheckCache = new Map();
+const DOMAIN_CACHE_MAX_SIZE = 500;
+
+// Helper to check if URL matches any domain
+function matchesDomain(url: string, domains: Set): boolean {
+ const cacheKey = url;
+ if (domainCheckCache.has(cacheKey)) {
+ return domainCheckCache.get(cacheKey)!;
+ }
+
+ const urlLower = url.toLowerCase();
+ let result = false;
+ for (const domain of domains) {
+ if (urlLower.includes(domain)) {
+ result = true;
+ break;
+ }
+ }
+
+ // Cache with size limit
+ if (domainCheckCache.size >= DOMAIN_CACHE_MAX_SIZE) {
+ const firstKey = domainCheckCache.keys().next().value;
+ if (firstKey !== undefined) {
+ domainCheckCache.delete(firstKey);
+ }
+ }
+ domainCheckCache.set(cacheKey, result);
+ return result;
+}
+
+// Precompiled patterns for CAPTCHA iframe detection
+const CAPTCHA_IFRAME_PATTERNS = ["recaptcha", "hcaptcha", "challenges.cloudflare.com", "turnstile"];
+
+// Precompiled patterns for important cookies
+const IMPORTANT_COOKIE_PATTERNS = ["_GRECAPTCHA", "h-captcha", "cf_", "session", "auth", "token"];
+
+// Track if handlers have been initialized to avoid duplicate setup
+let handlersInitialized = false;
/**
* Initialize CAPTCHA handlers on page load
@@ -44,39 +84,69 @@ const HEAVY_COOKIE_DOMAINS = [
export function initializeCaptchaHandlers() {
if (typeof window === "undefined") return;
+ // Prevent duplicate initialization
+ if (handlersInitialized) return;
+ handlersInitialized = true;
+
// Ensure global CAPTCHA callbacks are accessible
if (!window.___grecaptcha_cfg) {
window.___grecaptcha_cfg = { clients: {} };
}
- // Monitor for CAPTCHA iframe creation and ensure proper setup
+ // Debounced batch processing for mutations
+ let pendingNodes: Node[] = [];
+ let processingScheduled = false;
+
+ const processPendingNodes = () => {
+ processingScheduled = false;
+ const nodesToProcess = pendingNodes;
+ pendingNodes = [];
+
+ for (const node of nodesToProcess) {
+ if (node instanceof HTMLIFrameElement) {
+ const src = node.src || "";
+ // Check if this is a CAPTCHA iframe using precompiled patterns
+ const isCaptchaIframe = CAPTCHA_IFRAME_PATTERNS.some((pattern) =>
+ src.includes(pattern)
+ );
+
+ if (isCaptchaIframe) {
+ // Ensure the iframe has proper sandbox permissions
+ if (node.sandbox && node.sandbox.length > 0) {
+ node.sandbox.add("allow-same-origin");
+ node.sandbox.add("allow-scripts");
+ node.sandbox.add("allow-forms");
+ }
+
+ // Ensure credentials are included for CAPTCHA cookies
+ if (node.getAttribute("credentialless") !== null) {
+ node.removeAttribute("credentialless");
+ }
+ }
+ }
+ }
+ };
+
+ // Monitor for CAPTCHA iframe creation with batched processing
const observer = new MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- mutation.addedNodes.forEach((node) => {
+ for (const mutation of mutations) {
+ for (const node of mutation.addedNodes) {
if (node instanceof HTMLIFrameElement) {
- const src = node.src || "";
- // Check if this is a CAPTCHA iframe
- if (
- src.includes("recaptcha") ||
- src.includes("hcaptcha") ||
- src.includes("challenges.cloudflare.com") ||
- src.includes("turnstile")
- ) {
- // Ensure the iframe has proper sandbox permissions
- if (node.sandbox && node.sandbox.length > 0) {
- node.sandbox.add("allow-same-origin");
- node.sandbox.add("allow-scripts");
- node.sandbox.add("allow-forms");
- }
-
- // Ensure credentials are included for CAPTCHA cookies
- if (node.getAttribute("credentialless") !== null) {
- node.removeAttribute("credentialless");
- }
- }
+ pendingNodes.push(node);
}
- });
- });
+ }
+ }
+
+ // Schedule batch processing
+ if (pendingNodes.length > 0 && !processingScheduled) {
+ processingScheduled = true;
+ // Use requestIdleCallback if available, otherwise setTimeout
+ if ("requestIdleCallback" in window) {
+ (window as any).requestIdleCallback(processPendingNodes, { timeout: 100 });
+ } else {
+ setTimeout(processPendingNodes, 0);
+ }
+ }
});
// Start observing the document for changes
@@ -110,16 +180,10 @@ function enhanceCookieHandling() {
set(value) {
// Ensure SameSite=None for cookies in cross-origin contexts
if (typeof value === "string") {
- // Check if this is a CAPTCHA or heavy cookie site cookie
- const isCaptchaCookie =
- value.includes("_GRECAPTCHA") ||
- value.includes("h-captcha") ||
- value.includes("cf_");
- const isImportantCookie =
- isCaptchaCookie ||
- value.includes("session") ||
- value.includes("auth") ||
- value.includes("token");
+ // Check if this is a CAPTCHA or heavy cookie site cookie using precompiled patterns
+ const isImportantCookie = IMPORTANT_COOKIE_PATTERNS.some((pattern) =>
+ value.includes(pattern)
+ );
if (isImportantCookie && !value.includes("SameSite")) {
value += "; SameSite=None; Secure";
@@ -149,8 +213,8 @@ function enhanceNetworkRequests() {
: input.toString();
// Check if this is a CAPTCHA-related or heavy cookie site request
- const isCaptchaRequest = CAPTCHA_DOMAINS.some((domain) => url.includes(domain));
- const isHeavyCookieRequest = HEAVY_COOKIE_DOMAINS.some((domain) => url.includes(domain));
+ const isCaptchaRequest = matchesDomain(url, CAPTCHA_DOMAINS);
+ const isHeavyCookieRequest = !isCaptchaRequest && matchesDomain(url, HEAVY_COOKIE_DOMAINS);
if (isCaptchaRequest || isHeavyCookieRequest) {
// Ensure credentials are included
@@ -185,10 +249,9 @@ function enhanceNetworkRequests() {
const originalOpen = xhr.open;
xhr.open = function (method: string, url: string | URL, ...args: any[]) {
const urlStr = url.toString();
- const isCaptchaRequest = CAPTCHA_DOMAINS.some((domain) => urlStr.includes(domain));
- const isHeavyCookieRequest = HEAVY_COOKIE_DOMAINS.some((domain) =>
- urlStr.includes(domain)
- );
+ const isCaptchaRequest = matchesDomain(urlStr, CAPTCHA_DOMAINS);
+ const isHeavyCookieRequest =
+ !isCaptchaRequest && matchesDomain(urlStr, HEAVY_COOKIE_DOMAINS);
if (isCaptchaRequest || isHeavyCookieRequest) {
// Ensure credentials are included
diff --git a/src/utils/experimental.ts b/src/utils/experimental.ts
new file mode 100644
index 0000000..70ec82c
--- /dev/null
+++ b/src/utils/experimental.ts
@@ -0,0 +1,325 @@
+/**
+ * Experimental Features Module
+ *
+ * This module implements the experimental features for the proxy:
+ * 1. Preconfigured Sites - Automatically use optimal proxy configurations for known problematic sites
+ * 2. Dynamic Loading - Monitor for errors and automatically try different configurations
+ */
+
+import { StoreManager } from "./storage";
+
+// Preconfigured sites with their optimal proxy settings
+// Format: { domain: { proxy: "uv" | "sj", routingMode: "wisp" | "bare", transport?: "epoxy" | "libcurl" } }
+export const PRECONFIGURED_SITES: Record<
+ string,
+ {
+ proxy: "uv" | "sj";
+ routingMode: "wisp" | "bare";
+ transport?: "epoxy" | "libcurl";
+ reason?: string;
+ }
+> = {
+ // E-commerce sites that work better with Scramjet + Bare
+ "amazon.com": {
+ proxy: "sj",
+ routingMode: "bare",
+ reason: "Complex cookie handling and bot detection"
+ },
+ "amazon.co.uk": { proxy: "sj", routingMode: "bare", reason: "Regional Amazon variant" },
+ "amazon.de": { proxy: "sj", routingMode: "bare", reason: "Regional Amazon variant" },
+ "ebay.com": {
+ proxy: "sj",
+ routingMode: "bare",
+ reason: "Heavy JavaScript and session management"
+ },
+ "walmart.com": { proxy: "sj", routingMode: "bare", reason: "Bot detection systems" },
+
+ // Social media that works better with Ultraviolet
+ "twitter.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "WebSocket-heavy application"
+ },
+ "x.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "WebSocket-heavy application"
+ },
+ "instagram.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "Media-heavy with complex API"
+ },
+ "facebook.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "Complex single-page application"
+ },
+
+ // Video streaming sites
+ "youtube.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "libcurl",
+ reason: "Video streaming optimization"
+ },
+ "netflix.com": { proxy: "sj", routingMode: "bare", reason: "DRM and authentication" },
+ "twitch.tv": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "WebSocket-based chat and streaming"
+ },
+
+ // Gaming sites
+ "discord.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "WebSocket-heavy for real-time communication"
+ },
+ "roblox.com": { proxy: "sj", routingMode: "bare", reason: "Complex game client requirements" },
+
+ // Search engines
+ "google.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "Standard web search"
+ },
+ "bing.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "Standard web search"
+ },
+ "duckduckgo.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "Privacy-focused search"
+ },
+
+ // News and content sites
+ "reddit.com": {
+ proxy: "uv",
+ routingMode: "wisp",
+ transport: "epoxy",
+ reason: "Single-page application with infinite scroll"
+ },
+ "medium.com": { proxy: "uv", routingMode: "wisp", reason: "Content-focused site" },
+ "wikipedia.org": { proxy: "uv", routingMode: "wisp", reason: "Simple content delivery" }
+};
+
+// Configuration attempt order for dynamic loading
+export const CONFIGURATION_ATTEMPTS = [
+ { proxy: "uv" as const, routingMode: "wisp" as const, transport: "epoxy" as const },
+ { proxy: "uv" as const, routingMode: "wisp" as const, transport: "libcurl" as const },
+ { proxy: "sj" as const, routingMode: "wisp" as const, transport: "epoxy" as const },
+ { proxy: "sj" as const, routingMode: "bare" as const },
+ { proxy: "uv" as const, routingMode: "bare" as const }
+];
+
+/**
+ * Extract the domain from a URL
+ */
+export function extractDomain(url: string): string | null {
+ try {
+ const urlObj = new URL(url);
+ return urlObj.hostname.replace(/^www\./, "");
+ } catch {
+ // Try to extract domain from partial URL
+ const match = url.match(/(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
+ return match ? match[1].replace(/^www\./, "") : null;
+ }
+}
+
+/**
+ * Get the preconfigured settings for a URL if available
+ */
+export function getPreconfiguredSettings(url: string): (typeof PRECONFIGURED_SITES)[string] | null {
+ const domain = extractDomain(url);
+ if (!domain) return null;
+
+ // Check exact match first
+ if (PRECONFIGURED_SITES[domain]) {
+ return PRECONFIGURED_SITES[domain];
+ }
+
+ // Check for subdomain matches
+ for (const configuredDomain of Object.keys(PRECONFIGURED_SITES)) {
+ if (domain.endsWith("." + configuredDomain) || domain === configuredDomain) {
+ return PRECONFIGURED_SITES[configuredDomain];
+ }
+ }
+
+ return null;
+}
+
+/**
+ * Check if preconfigured sites feature is enabled
+ */
+export function isPreconfiguredSitesEnabled(): boolean {
+ const storage = new StoreManager<"radius||settings">("radius||settings");
+ return storage.getVal("experimentPreconfiguredSites") === "true";
+}
+
+/**
+ * Check if dynamic loading feature is enabled
+ */
+export function isDynamicLoadingEnabled(): boolean {
+ const storage = new StoreManager<"radius||settings">("radius||settings");
+ return storage.getVal("experimentDynamicLoading") === "true";
+}
+
+// Track current configuration attempt index for dynamic loading
+let currentAttemptIndex = 0;
+let lastFailedUrl = "";
+
+/**
+ * Get the next configuration to try for dynamic loading
+ * Returns null if all configurations have been tried
+ */
+export function getNextConfiguration(): (typeof CONFIGURATION_ATTEMPTS)[number] | null {
+ if (currentAttemptIndex >= CONFIGURATION_ATTEMPTS.length) {
+ return null;
+ }
+ return CONFIGURATION_ATTEMPTS[currentAttemptIndex++];
+}
+
+/**
+ * Reset the configuration attempt counter
+ */
+export function resetConfigurationAttempts(url?: string): void {
+ if (url !== lastFailedUrl) {
+ currentAttemptIndex = 0;
+ lastFailedUrl = url || "";
+ }
+}
+
+/**
+ * Check if all configurations have been exhausted
+ */
+export function allConfigurationsExhausted(): boolean {
+ return currentAttemptIndex >= CONFIGURATION_ATTEMPTS.length;
+}
+
+// Error patterns that indicate a configuration issue
+const ERROR_PATTERNS = [
+ "Failed to fetch",
+ "NetworkError",
+ "net::ERR_",
+ "TypeError: Failed to fetch",
+ "AbortError",
+ "Connection refused",
+ "CORS error",
+ "blocked by CORS",
+ "WebSocket connection failed",
+ "Service Worker Error"
+];
+
+/**
+ * Check if an error message indicates a configuration issue
+ */
+export function isConfigurationError(errorMessage: string): boolean {
+ const lowerMessage = errorMessage.toLowerCase();
+ return ERROR_PATTERNS.some((pattern) => lowerMessage.includes(pattern.toLowerCase()));
+}
+
+/**
+ * Dynamic loading error handler class
+ * Monitors console errors and triggers configuration changes
+ */
+export class DynamicLoadingHandler {
+ private storage: StoreManager<"radius||settings">;
+ private originalConsoleError: typeof console.error;
+ private errorCount: number = 0;
+ private readonly ERROR_THRESHOLD = 3;
+ private readonly ERROR_WINDOW = 5000; // 5 seconds
+ private lastErrorTime: number = 0;
+ private isActive: boolean = false;
+ private onConfigChange?: (config: (typeof CONFIGURATION_ATTEMPTS)[number]) => Promise;
+ private onAllFailed?: () => void;
+
+ constructor() {
+ this.storage = new StoreManager<"radius||settings">("radius||settings");
+ this.originalConsoleError = console.error;
+ }
+
+ /**
+ * Start monitoring for errors
+ */
+ start(
+ onConfigChange: (config: (typeof CONFIGURATION_ATTEMPTS)[number]) => Promise,
+ onAllFailed: () => void
+ ): void {
+ if (this.isActive) return;
+ this.isActive = true;
+ this.onConfigChange = onConfigChange;
+ this.onAllFailed = onAllFailed;
+
+ // Override console.error to catch proxy errors
+ console.error = (...args: any[]) => {
+ this.originalConsoleError.apply(console, args);
+ this.handleError(args.map((a) => String(a)).join(" "));
+ };
+
+ // Listen for unhandled errors
+ window.addEventListener("error", this.handleWindowError);
+ window.addEventListener("unhandledrejection", this.handleUnhandledRejection);
+ }
+
+ /**
+ * Stop monitoring for errors
+ */
+ stop(): void {
+ if (!this.isActive) return;
+ this.isActive = false;
+
+ console.error = this.originalConsoleError;
+ window.removeEventListener("error", this.handleWindowError);
+ window.removeEventListener("unhandledrejection", this.handleUnhandledRejection);
+ }
+
+ private handleWindowError = (event: ErrorEvent): void => {
+ this.handleError(event.message);
+ };
+
+ private handleUnhandledRejection = (event: PromiseRejectionEvent): void => {
+ this.handleError(String(event.reason));
+ };
+
+ private handleError(message: string): void {
+ if (!isConfigurationError(message)) return;
+
+ const now = Date.now();
+
+ // Reset error count if outside the window
+ if (now - this.lastErrorTime > this.ERROR_WINDOW) {
+ this.errorCount = 0;
+ }
+
+ this.errorCount++;
+ this.lastErrorTime = now;
+
+ // If we've hit the threshold, try a new configuration
+ if (this.errorCount >= this.ERROR_THRESHOLD) {
+ this.errorCount = 0;
+ this.tryNextConfiguration();
+ }
+ }
+
+ private async tryNextConfiguration(): Promise {
+ const nextConfig = getNextConfiguration();
+
+ if (nextConfig && this.onConfigChange) {
+ await this.onConfigChange(nextConfig);
+ } else if (this.onAllFailed) {
+ this.onAllFailed();
+ }
+ }
+}
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts
index 6fd13d4..290bef2 100644
--- a/src/utils/proxy.ts
+++ b/src/utils/proxy.ts
@@ -1,11 +1,40 @@
import { BareMuxConnection } from "@mercuryworkshop/bare-mux";
import { StoreManager } from "./storage";
import { initializeCaptchaHandlers } from "./captcha-handler";
+import {
+ getPreconfiguredSettings,
+ isPreconfiguredSitesEnabled,
+ isDynamicLoadingEnabled,
+ DynamicLoadingHandler,
+ resetConfigurationAttempts
+} from "./experimental";
+
+// Cache for URL encoding to avoid redundant computations
+const urlEncodingCache = new Map();
+const URL_CACHE_MAX_SIZE = 500;
+
+// Cache for transport settings to avoid redundant storage reads
+let cachedTransportSettings: {
+ transport?: string;
+ routingMode?: string;
+ wispServer?: string;
+ adBlock?: string;
+ lastUpdate: number;
+} | null = null;
+const TRANSPORT_CACHE_TTL = 5000; // 5 seconds
+
+const createScript = (src: string, defer?: boolean): HTMLScriptElement => {
+ // Check if script already exists to avoid duplicate loading
+ const existingScript = document.querySelector(`script[src="${src}"]`);
+ if (existingScript) {
+ return existingScript as HTMLScriptElement;
+ }
-const createScript = (src: string, defer?: boolean) => {
const script = document.createElement("script") as HTMLScriptElement;
script.src = src;
if (defer) script.defer = defer;
+ // Add async loading for better performance
+ script.async = false; // Keep execution order
return document.body.appendChild(script);
};
@@ -31,6 +60,7 @@ class SW {
#serviceWorker?: ServiceWorkerRegistration;
#storageManager: StoreManager<"radius||settings">;
#ready: Promise;
+ #dynamicLoadingHandler?: DynamicLoadingHandler;
static #instance = new Set();
static *getInstance() {
@@ -59,20 +89,131 @@ class SW {
return template.replace("%s", encodeURIComponent(input));
}
+ /**
+ * Apply preconfigured settings for a URL if available and enabled
+ */
+ async applyPreconfiguredSettings(url: string): Promise {
+ if (!isPreconfiguredSitesEnabled()) return false;
+
+ const settings = getPreconfiguredSettings(url);
+ if (!settings) return false;
+
+ // Apply the preconfigured settings
+ this.#storageManager.setVal("proxy", settings.proxy);
+
+ if (settings.routingMode) {
+ await this.routingMode(settings.routingMode, false);
+ }
+
+ if (settings.transport) {
+ await this.setTransport(settings.transport);
+ } else {
+ await this.setTransport();
+ }
+
+ this.#invalidateTransportCache();
+ return true;
+ }
+
encodeURL(string: string): string {
+ // Check cache first
+ const cacheKey = `${this.#storageManager.getVal("proxy") || "uv"}:${string}`;
+ const cached = urlEncodingCache.get(cacheKey);
+ if (cached) {
+ return cached;
+ }
+
const proxy = this.#storageManager.getVal("proxy") as "uv" | "sj";
const input = this.search(string, this.#storageManager.getVal("searchEngine"));
- return proxy === "uv"
- ? `${__uv$config.prefix}${__uv$config.encodeUrl!(input)}`
- : this.#scramjetController!.encodeUrl(input);
+ const encoded =
+ proxy === "uv"
+ ? `${__uv$config.prefix}${__uv$config.encodeUrl!(input)}`
+ : this.#scramjetController!.encodeUrl(input);
+
+ // Cache the result
+ if (urlEncodingCache.size >= URL_CACHE_MAX_SIZE) {
+ // Remove oldest entries
+ const keysToDelete = Array.from(urlEncodingCache.keys()).slice(0, 50);
+ keysToDelete.forEach((key) => urlEncodingCache.delete(key));
+ }
+ urlEncodingCache.set(cacheKey, encoded);
+
+ return encoded;
+ }
+
+ /**
+ * Encode URL with experimental features support
+ * This method applies preconfigured settings if enabled before encoding
+ */
+ async encodeURLWithExperiments(url: string): Promise {
+ // Reset configuration attempts for new URL
+ resetConfigurationAttempts(url);
+
+ // Apply preconfigured settings if available
+ await this.applyPreconfiguredSettings(url);
+
+ // Start dynamic loading if enabled
+ if (isDynamicLoadingEnabled() && !this.#dynamicLoadingHandler) {
+ this.#dynamicLoadingHandler = new DynamicLoadingHandler();
+ this.#dynamicLoadingHandler.start(
+ async (config) => {
+ // Apply new configuration
+ this.#storageManager.setVal("proxy", config.proxy);
+ await this.routingMode(config.routingMode, false);
+ if (config.transport) {
+ await this.setTransport(config.transport);
+ } else {
+ await this.setTransport();
+ }
+ this.#invalidateTransportCache();
+ // Clear URL cache to re-encode with new settings
+ urlEncodingCache.clear();
+ },
+ () => {
+ // All configurations failed - redirect to 404
+ window.location.href = "/404";
+ }
+ );
+ }
+
+ return this.encodeURL(url);
+ }
+
+ // Helper method to get cached transport settings
+ #getTransportSettings() {
+ const now = Date.now();
+ if (
+ cachedTransportSettings &&
+ now - cachedTransportSettings.lastUpdate < TRANSPORT_CACHE_TTL
+ ) {
+ return cachedTransportSettings;
+ }
+
+ cachedTransportSettings = {
+ transport: this.#storageManager.getVal("transport"),
+ routingMode: this.#storageManager.getVal("routingMode"),
+ wispServer: this.#storageManager.getVal("wispServer"),
+ adBlock: this.#storageManager.getVal("adBlock"),
+ lastUpdate: now
+ };
+
+ return cachedTransportSettings;
+ }
+
+ // Invalidate transport cache when settings change
+ #invalidateTransportCache() {
+ cachedTransportSettings = null;
}
async setTransport(transport?: "epoxy" | "libcurl", get?: boolean) {
- console.log("Setting transport");
- const routingMode = this.#storageManager.getVal("routingMode") || "wisp";
+ const settings = this.#getTransportSettings();
+ const routingMode = settings.routingMode || "wisp";
+
const wispServer = (): string => {
- const wispServerVal = this.#storageManager.getVal("wispServer");
- if (this.#storageManager.getVal("adBlock") === "true") {
+ const wispServerVal =
+ settings.wispServer ||
+ (location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/wisp/";
+ if (settings.adBlock === "true") {
return wispServerVal.replace("/wisp/", "/adblock/");
}
return wispServerVal;
@@ -82,37 +223,21 @@ class SW {
(location.protocol === "https:" ? "https://" : "http://") + location.host + "/bare/"
);
};
- if (get) return this.#storageManager.getVal("transport");
- this.#storageManager.setVal(
- "transport",
- transport || this.#storageManager.getVal("transport") || "epoxy"
- );
+ if (get) return settings.transport;
+
+ const newTransport = transport || settings.transport || "epoxy";
+ this.#storageManager.setVal("transport", newTransport);
+ this.#invalidateTransportCache();
if (routingMode === "bare") {
// Use bare server transport
await this.#baremuxConn!.setTransport("/baremod/index.mjs", [bareServer()]);
} else {
// Use wisp server transport (default)
- switch (transport) {
- case "epoxy": {
- await this.#baremuxConn!.setTransport("/epoxy/index.mjs", [
- { wisp: wispServer() }
- ]);
- break;
- }
- case "libcurl": {
- await this.#baremuxConn!.setTransport("/libcurl/index.mjs", [
- { wisp: wispServer() }
- ]);
- break;
- }
- default: {
- await this.#baremuxConn!.setTransport("/epoxy/index.mjs", [
- { wisp: wispServer() }
- ]);
- break;
- }
- }
+ // Optimize transport path selection
+ const transportPath =
+ newTransport === "libcurl" ? "/libcurl/index.mjs" : "/epoxy/index.mjs";
+ await this.#baremuxConn!.setTransport(transportPath, [{ wisp: wispServer() }]);
}
}
@@ -121,25 +246,29 @@ class SW {
"routingMode",
mode || this.#storageManager.getVal("routingMode") || "wisp"
);
+ this.#invalidateTransportCache();
if (set) await this.setTransport();
}
async wispServer(wispServer?: string, set?: true) {
- console.log(wispServer?.replace("/wisp/", "/adblock/"));
this.#storageManager.setVal(
"wispServer",
wispServer ||
this.#storageManager.getVal("wispServer") ||
(location.protocol === "https:" ? "wss://" : "ws://") + location.host + "/wisp/"
);
+ this.#invalidateTransportCache();
if (set) await this.setTransport();
}
constructor() {
SW.#instance.add(this);
this.#storageManager = new StoreManager("radius||settings");
+
+ // Optimized script loading check with debounced interval
const checkScripts = (): Promise => {
return new Promise((resolve) => {
+ // Use a reasonable interval (16ms = ~60fps) instead of default
const t = setInterval(() => {
if (
typeof __uv$config !== "undefined" &&
@@ -148,9 +277,11 @@ class SW {
clearInterval(t);
resolve();
}
- });
+ }, 16);
});
};
+
+ // Load scripts in optimal order
createScript("/vu/uv.bundle.js", true);
createScript("/vu/uv.config.js", true);
createScript("/marcs/scramjet.all.js", true);
@@ -184,7 +315,6 @@ class SW {
if ("serviceWorker" in navigator) {
await this.#scramjetController.init();
navigator.serviceWorker.ready.then(async (reg) => {
- console.log("SW ready to go!");
this.#serviceWorker = reg;
// Initialize CAPTCHA handlers for automatic verification support
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
index 10f24e3..c15bbcf 100644
--- a/src/utils/storage.ts
+++ b/src/utils/storage.ts
@@ -1,16 +1,61 @@
+// Memory cache for frequently accessed values
+const storageCache = new Map();
+const STORAGE_CACHE_TTL = 5000; // 5 seconds
+const STORAGE_CACHE_MAX_SIZE = 100;
+
class StoreManager {
#prefix: string;
+
constructor(pref: Prefix) {
this.#prefix = pref;
}
+
getVal(key: string): string {
- return localStorage.getItem(`${this.#prefix}||${key}`) as string;
+ const fullKey = `${this.#prefix}||${key}`;
+
+ // Check cache first
+ const cached = storageCache.get(fullKey);
+ const now = Date.now();
+ if (cached && now - cached.timestamp < STORAGE_CACHE_TTL) {
+ return cached.value;
+ }
+
+ // Get from localStorage
+ const value = localStorage.getItem(fullKey) as string;
+
+ // Update cache
+ if (storageCache.size >= STORAGE_CACHE_MAX_SIZE) {
+ const firstKey = storageCache.keys().next().value;
+ if (firstKey !== undefined) {
+ storageCache.delete(firstKey);
+ }
+ }
+ if (value !== null) {
+ storageCache.set(fullKey, { value, timestamp: now });
+ }
+
+ return value;
}
+
setVal(key: string, val: string): void {
- localStorage.setItem(`${this.#prefix}||${key}`, val);
+ const fullKey = `${this.#prefix}||${key}`;
+ localStorage.setItem(fullKey, val);
+
+ // Update cache
+ storageCache.set(fullKey, { value: val, timestamp: Date.now() });
}
+
removeVal(key: string): void {
- localStorage.removeItem(`${this.#prefix}||${key}`);
+ const fullKey = `${this.#prefix}||${key}`;
+ localStorage.removeItem(fullKey);
+
+ // Remove from cache
+ storageCache.delete(fullKey);
+ }
+
+ // Clear cache when needed (e.g., for debugging)
+ static clearCache(): void {
+ storageCache.clear();
}
}
From 839e7ed8fb31450f19be5dc0f13f2aba664ec115 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 26 Nov 2025 23:35:48 +0000
Subject: [PATCH 3/3] Address code review feedback - improve cache eviction
strategies
Co-authored-by: sriail <225764385+sriail@users.noreply.github.com>
---
public/sw.js | 36 +++++++++++++++++++++++++-----------
server/index.ts | 10 +++++++---
src/utils/captcha-handler.ts | 11 +++++++----
src/utils/proxy.ts | 13 +++++++++----
src/utils/storage.ts | 12 ++++++++----
5 files changed, 56 insertions(+), 26 deletions(-)
diff --git a/public/sw.js b/public/sw.js
index d350547..31ad960 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -21,9 +21,11 @@ const sj = new ScramjetServiceWorker({
const routeCache = new Map();
const ROUTE_CACHE_MAX_SIZE = 500;
const ROUTE_CACHE_TTL = 30000; // 30 seconds
+const CACHE_EVICT_COUNT = 50; // Evict 10% of cache
// Cache for domain checks to avoid redundant string operations
const domainCheckCache = new Map();
+const DOMAIN_CHECK_CACHE_MAX_SIZE = 500;
// Enhanced CAPTCHA and Cloudflare verification support
// Use Set for O(1) lookup instead of Array with some()
@@ -73,10 +75,14 @@ function isCaptchaRequest(url) {
}
}
- // Cache with size limit
- if (domainCheckCache.size >= 1000) {
- const firstKey = domainCheckCache.keys().next().value;
- domainCheckCache.delete(firstKey);
+ // Cache with size limit and batch eviction
+ if (domainCheckCache.size >= DOMAIN_CHECK_CACHE_MAX_SIZE) {
+ let evicted = 0;
+ for (const key of domainCheckCache.keys()) {
+ if (evicted >= CACHE_EVICT_COUNT) break;
+ domainCheckCache.delete(key);
+ evicted++;
+ }
}
domainCheckCache.set(cacheKey, result);
return result;
@@ -98,10 +104,14 @@ function isHeavyCookieSite(url) {
}
}
- // Cache with size limit
- if (domainCheckCache.size >= 1000) {
- const firstKey = domainCheckCache.keys().next().value;
- domainCheckCache.delete(firstKey);
+ // Cache with size limit and batch eviction
+ if (domainCheckCache.size >= DOMAIN_CHECK_CACHE_MAX_SIZE) {
+ let evicted = 0;
+ for (const key of domainCheckCache.keys()) {
+ if (evicted >= CACHE_EVICT_COUNT) break;
+ domainCheckCache.delete(key);
+ evicted++;
+ }
}
domainCheckCache.set(cacheKey, result);
return result;
@@ -175,10 +185,14 @@ self.addEventListener("fetch", function (event) {
timestamp: now
};
- // Cache with size limit
+ // Cache with size limit and batch eviction
if (routeCache.size >= ROUTE_CACHE_MAX_SIZE) {
- const firstKey = routeCache.keys().next().value;
- routeCache.delete(firstKey);
+ let evicted = 0;
+ for (const key of routeCache.keys()) {
+ if (evicted >= CACHE_EVICT_COUNT) break;
+ routeCache.delete(key);
+ evicted++;
+ }
}
routeCache.set(url, routeInfo);
}
diff --git a/server/index.ts b/server/index.ts
index a66d97d..9a149aa 100644
--- a/server/index.ts
+++ b/server/index.ts
@@ -24,9 +24,13 @@ const ROUTING_CACHE_TTL = 60000; // 1 minute
// Helper to manage routing cache with TTL and size limit
const cacheRoutingDecision = (url: string, decision: "bare" | "wisp" | "static"): void => {
if (routingCache.size >= ROUTING_CACHE_MAX_SIZE) {
- // Remove oldest entries (first 100)
- const keysToDelete = Array.from(routingCache.keys()).slice(0, 100);
- keysToDelete.forEach((key) => routingCache.delete(key));
+ // Remove oldest entries using iterator to avoid array allocation
+ let evicted = 0;
+ for (const key of routingCache.keys()) {
+ if (evicted >= 100) break;
+ routingCache.delete(key);
+ evicted++;
+ }
}
routingCache.set(url, decision);
};
diff --git a/src/utils/captcha-handler.ts b/src/utils/captcha-handler.ts
index e45d8a7..832960e 100644
--- a/src/utils/captcha-handler.ts
+++ b/src/utils/captcha-handler.ts
@@ -39,6 +39,7 @@ const HEAVY_COOKIE_DOMAINS = new Set([
// Cache for domain checks to avoid redundant operations
const domainCheckCache = new Map();
const DOMAIN_CACHE_MAX_SIZE = 500;
+const DOMAIN_CACHE_EVICT_COUNT = 50; // Evict 10% of cache
// Helper to check if URL matches any domain
function matchesDomain(url: string, domains: Set): boolean {
@@ -56,11 +57,13 @@ function matchesDomain(url: string, domains: Set): boolean {
}
}
- // Cache with size limit
+ // Cache with size limit and batch eviction
if (domainCheckCache.size >= DOMAIN_CACHE_MAX_SIZE) {
- const firstKey = domainCheckCache.keys().next().value;
- if (firstKey !== undefined) {
- domainCheckCache.delete(firstKey);
+ let evicted = 0;
+ for (const key of domainCheckCache.keys()) {
+ if (evicted >= DOMAIN_CACHE_EVICT_COUNT) break;
+ domainCheckCache.delete(key);
+ evicted++;
}
}
domainCheckCache.set(cacheKey, result);
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts
index 290bef2..33b917e 100644
--- a/src/utils/proxy.ts
+++ b/src/utils/proxy.ts
@@ -12,6 +12,7 @@ import {
// Cache for URL encoding to avoid redundant computations
const urlEncodingCache = new Map();
const URL_CACHE_MAX_SIZE = 500;
+const URL_CACHE_EVICT_COUNT = 50; // Evict 10% of cache
// Cache for transport settings to avoid redundant storage reads
let cachedTransportSettings: {
@@ -130,11 +131,15 @@ class SW {
? `${__uv$config.prefix}${__uv$config.encodeUrl!(input)}`
: this.#scramjetController!.encodeUrl(input);
- // Cache the result
+ // Cache the result with iterator-based eviction
if (urlEncodingCache.size >= URL_CACHE_MAX_SIZE) {
- // Remove oldest entries
- const keysToDelete = Array.from(urlEncodingCache.keys()).slice(0, 50);
- keysToDelete.forEach((key) => urlEncodingCache.delete(key));
+ // Remove oldest entries using iterator to avoid array allocation
+ let evicted = 0;
+ for (const key of urlEncodingCache.keys()) {
+ if (evicted >= URL_CACHE_EVICT_COUNT) break;
+ urlEncodingCache.delete(key);
+ evicted++;
+ }
}
urlEncodingCache.set(cacheKey, encoded);
diff --git a/src/utils/storage.ts b/src/utils/storage.ts
index c15bbcf..8d61852 100644
--- a/src/utils/storage.ts
+++ b/src/utils/storage.ts
@@ -2,6 +2,7 @@
const storageCache = new Map();
const STORAGE_CACHE_TTL = 5000; // 5 seconds
const STORAGE_CACHE_MAX_SIZE = 100;
+const STORAGE_CACHE_EVICT_COUNT = 20; // Evict 20% of cache
class StoreManager {
#prefix: string;
@@ -23,11 +24,14 @@ class StoreManager {
// Get from localStorage
const value = localStorage.getItem(fullKey) as string;
- // Update cache
+ // Update cache with batch eviction
if (storageCache.size >= STORAGE_CACHE_MAX_SIZE) {
- const firstKey = storageCache.keys().next().value;
- if (firstKey !== undefined) {
- storageCache.delete(firstKey);
+ // Remove oldest entries using iterator to avoid array allocation
+ let evicted = 0;
+ for (const cacheKey of storageCache.keys()) {
+ if (evicted >= STORAGE_CACHE_EVICT_COUNT) break;
+ storageCache.delete(cacheKey);
+ evicted++;
}
}
if (value !== null) {