From 223d711646d1dc61e32237a5ecee7d1635ea54c7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 27 Nov 2025 14:10:57 +0000
Subject: [PATCH 1/3] Initial plan
From c162d9891b3629978085894b2e0ed8d88ca16f9e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 27 Nov 2025 14:18:30 +0000
Subject: [PATCH 2/3] Remove preconfigured site option and add dynamic loading
feature
Co-authored-by: sriail <225764385+sriail@users.noreply.github.com>
---
src/pages/index.astro | 28 +-
src/pages/settings/experiments.astro | 28 +-
src/utils/dynamic-loading.ts | 480 +++++++++++++++++++++++++++
src/utils/proxy.ts | 18 +-
4 files changed, 528 insertions(+), 26 deletions(-)
create mode 100644 src/utils/dynamic-loading.ts
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 81c8b22..1110388 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -41,6 +41,7 @@ const link = Astro.url.searchParams.get("redir");
import { Settings } from "@utils/settings.ts";
import { BareClient } from "@mercuryworkshop/bare-mux";
import { setupIframeInterceptor } from "@utils/iframe-interceptor.ts";
+ import { getDynamicLoading } from "@utils/dynamic-loading.ts";
const init = async () => {
const input = document.getElementById("input") as HTMLInputElement;
@@ -64,9 +65,15 @@ const link = Astro.url.searchParams.get("redir");
const sw = SW.getInstance().next().value!;
await sw.ready();
+ // Initialize dynamic loading
+ const dynamicLoading = getDynamicLoading();
+
// Setup iframe interceptor to prevent new tabs/windows
setupIframeInterceptor(iframe, (url: string) => sw.encodeURL(url));
+ // Track current site URL for dynamic loading
+ let currentSiteUrl: string | null = null;
+
input.addEventListener("keypress", async (event: any) => {
if (event.key === "Enter") {
// Handle "about:" URLs
@@ -88,6 +95,18 @@ const link = Astro.url.searchParams.get("redir");
}
const settings = await Settings.getInstance();
+
+ // If leaving a previous site, restore original config
+ if (currentSiteUrl && dynamicLoading.isEnabled()) {
+ await dynamicLoading.onSiteLeave();
+ }
+
+ // Notify dynamic loading of new site
+ currentSiteUrl = inputValue;
+ if (dynamicLoading.isEnabled()) {
+ await dynamicLoading.onSiteEnter(inputValue);
+ }
+
iframe.classList.remove("hidden");
iframe.src = sw.encodeURL(input.value);
buttons();
@@ -133,7 +152,14 @@ const link = Astro.url.searchParams.get("redir");
phlImage.src = object;
bhl.classList.add("hidden");
phl.classList.remove("hidden");
- });
+ });
+
+ // Handle page unload to restore config
+ window.addEventListener("beforeunload", async () => {
+ if (currentSiteUrl && dynamicLoading.isEnabled()) {
+ await dynamicLoading.onSiteLeave();
+ }
+ });
}
class CustomComponent extends HTMLElement {
diff --git a/src/pages/settings/experiments.astro b/src/pages/settings/experiments.astro
index 30e9e92..9fc48eb 100644
--- a/src/pages/settings/experiments.astro
+++ b/src/pages/settings/experiments.astro
@@ -13,16 +13,10 @@ import { Icon } from "astro-icon/components";
-
-
@@ -32,23 +26,14 @@ import { Icon } from "astro-icon/components";
import { StoreManager } from "@utils/storage";
const initExperiments = (storage: StoreManager<"radius||settings">) => {
- const preconfiguredSitesSwitch = document.getElementById("switch-preconfiguredSites");
const dynamicLoadingSwitch = document.getElementById("switch-dynamicLoading");
- if (!preconfiguredSitesSwitch || !dynamicLoadingSwitch) return;
+ if (!dynamicLoadingSwitch) return;
- // Load saved states
- const preconfiguredSitesEnabled = storage.getVal("experimentPreconfiguredSites") === "true";
+ // Load saved state
const dynamicLoadingEnabled = storage.getVal("experimentDynamicLoading") === "true";
- // Set initial states
- if (preconfiguredSitesEnabled) {
- preconfiguredSitesSwitch.setAttribute("aria-checked", "true");
- preconfiguredSitesSwitch.setAttribute("data-state", "checked");
- const thumb = preconfiguredSitesSwitch.querySelector("span");
- if (thumb) thumb.setAttribute("data-state", "checked");
- }
-
+ // Set initial state
if (dynamicLoadingEnabled) {
dynamicLoadingSwitch.setAttribute("aria-checked", "true");
dynamicLoadingSwitch.setAttribute("data-state", "checked");
@@ -57,11 +42,6 @@ import { Icon } from "astro-icon/components";
}
// Listen for changes
- preconfiguredSitesSwitch.addEventListener("switch-change", (e: Event) => {
- const customEvent = e as CustomEvent<{ checked: boolean }>;
- storage.setVal("experimentPreconfiguredSites", customEvent.detail.checked.toString());
- });
-
dynamicLoadingSwitch.addEventListener("switch-change", (e: Event) => {
const customEvent = e as CustomEvent<{ checked: boolean }>;
storage.setVal("experimentDynamicLoading", customEvent.detail.checked.toString());
diff --git a/src/utils/dynamic-loading.ts b/src/utils/dynamic-loading.ts
new file mode 100644
index 0000000..47e497e
--- /dev/null
+++ b/src/utils/dynamic-loading.ts
@@ -0,0 +1,480 @@
+import { StoreManager } from "./storage";
+import { BareMuxConnection } from "@mercuryworkshop/bare-mux";
+
+/**
+ * Proxy configuration type
+ */
+interface ProxyConfig {
+ proxy: "uv" | "sj";
+ transport: "epoxy" | "libcurl";
+ routingMode: "wisp" | "bare";
+}
+
+/**
+ * Error patterns that indicate a site failed to load completely
+ * These are critical errors that prevent page rendering
+ */
+const CRITICAL_ERROR_PATTERNS: RegExp[] = [
+ // JSON parsing errors
+ /unexpected\s+token.*json/i,
+ /json\s+parse\s+error/i,
+ /invalid\s+json/i,
+ /unexpected\s+end\s+of\s+json\s+input/i,
+ /syntaxerror.*json/i,
+
+ // Network/fetch errors that indicate complete failure
+ /failed\s+to\s+fetch/i,
+ /networkerror/i,
+ /typeerror:\s*failed\s+to\s+fetch/i,
+ /load\s+failed/i,
+ /net::err_/i,
+ /connection\s+refused/i,
+ /connection\s+reset/i,
+ /connection\s+timed\s+out/i,
+
+ // Proxy-specific errors
+ /bare\s+server\s+error/i,
+ /wisp\s+connection\s+failed/i,
+ /proxy\s+connection\s+failed/i,
+ /transport\s+error/i,
+ /service\s+worker\s+error/i,
+
+ // Script loading errors (critical for page render)
+ /script\s+error/i,
+ /failed\s+to\s+load\s+script/i,
+ /failed\s+to\s+load\s+resource.*script/i
+];
+
+/**
+ * Error patterns to ignore (minor errors that don't prevent page loading)
+ */
+const IGNORED_ERROR_PATTERNS: RegExp[] = [
+ // 404 errors for resources (not critical)
+ /404\s+not\s+found/i,
+ /failed\s+to\s+load\s+resource.*404/i,
+
+ // Analytics/tracking failures
+ /analytics/i,
+ /tracking/i,
+ /beacon/i,
+ /telemetry/i,
+
+ // Ad-related failures
+ /ads\./i,
+ /adservice/i,
+ /doubleclick/i,
+ /googlesyndication/i,
+
+ // Font loading failures
+ /font.*failed/i,
+ /woff2?.*failed/i,
+
+ // Image loading failures
+ /failed\s+to\s+load\s+resource.*image/i,
+ /failed\s+to\s+load\s+resource.*png/i,
+ /failed\s+to\s+load\s+resource.*jpg/i,
+ /failed\s+to\s+load\s+resource.*gif/i,
+ /failed\s+to\s+load\s+resource.*svg/i,
+
+ // Favicon failures
+ /favicon/i,
+
+ // CORS errors for non-essential resources
+ /cors.*font/i,
+ /cors.*image/i
+];
+
+/**
+ * All possible proxy configurations to try
+ */
+const PROXY_CONFIGS: ProxyConfig[] = [
+ { proxy: "uv", transport: "epoxy", routingMode: "wisp" },
+ { proxy: "uv", transport: "libcurl", routingMode: "wisp" },
+ { proxy: "uv", transport: "epoxy", routingMode: "bare" },
+ { proxy: "sj", transport: "epoxy", routingMode: "wisp" },
+ { proxy: "sj", transport: "libcurl", routingMode: "wisp" },
+ { proxy: "sj", transport: "epoxy", routingMode: "bare" }
+];
+
+/**
+ * DynamicLoading class handles automatic proxy configuration switching
+ * when a site fails to load due to critical errors.
+ *
+ * It monitors for errors, maintains site-specific override configurations,
+ * and reverts to the original configuration when leaving a site.
+ */
+class DynamicLoading {
+ #storageManager: StoreManager<"radius||settings">;
+ #baremuxConn: BareMuxConnection | null = null;
+ #currentSite: string | null = null;
+ #errorCount: number = 0;
+ #configIndex: number = 0;
+ #originalConfig: ProxyConfig | null = null;
+ #isEnabled: boolean = false;
+ #errorBuffer: string[] = [];
+ #errorCheckTimeout: number | null = null;
+ #siteOverrides: Map = new Map(); // Maps site domain to config index
+
+ // Error threshold before switching configs
+ static readonly ERROR_THRESHOLD = 3;
+ // Time window to collect errors (ms)
+ static readonly ERROR_WINDOW = 2000;
+ // Debounce time for error checking (ms)
+ static readonly DEBOUNCE_TIME = 500;
+
+ constructor() {
+ this.#storageManager = new StoreManager("radius||settings");
+ this.#isEnabled = this.#storageManager.getVal("experimentDynamicLoading") === "true";
+ this.#loadSiteOverrides();
+ }
+
+ /**
+ * Initialize the dynamic loading system
+ */
+ async init(baremuxConn: BareMuxConnection): Promise {
+ this.#baremuxConn = baremuxConn;
+ if (this.#isEnabled) {
+ this.#setupErrorListeners();
+ }
+ }
+
+ /**
+ * Check if dynamic loading is enabled
+ */
+ isEnabled(): boolean {
+ return this.#isEnabled;
+ }
+
+ /**
+ * Enable or disable dynamic loading
+ */
+ setEnabled(enabled: boolean): void {
+ this.#isEnabled = enabled;
+ this.#storageManager.setVal("experimentDynamicLoading", enabled.toString());
+ if (enabled) {
+ this.#setupErrorListeners();
+ }
+ }
+
+ /**
+ * Get the current original configuration
+ */
+ #getCurrentConfig(): ProxyConfig {
+ return {
+ proxy: (this.#storageManager.getVal("proxy") as "uv" | "sj") || "uv",
+ transport: (this.#storageManager.getVal("transport") as "epoxy" | "libcurl") || "epoxy",
+ routingMode: (this.#storageManager.getVal("routingMode") as "wisp" | "bare") || "wisp"
+ };
+ }
+
+ /**
+ * Load site-specific overrides from storage
+ */
+ #loadSiteOverrides(): void {
+ try {
+ const stored = this.#storageManager.getVal("dynamicLoadingOverrides");
+ if (stored) {
+ const parsed = JSON.parse(stored);
+ this.#siteOverrides = new Map(Object.entries(parsed));
+ }
+ } catch {
+ this.#siteOverrides = new Map();
+ }
+ }
+
+ /**
+ * Save site-specific overrides to storage
+ */
+ #saveSiteOverrides(): void {
+ const obj = Object.fromEntries(this.#siteOverrides);
+ this.#storageManager.setVal("dynamicLoadingOverrides", JSON.stringify(obj));
+ }
+
+ /**
+ * Extract domain from a URL for site identification
+ */
+ #extractDomain(url: string): string {
+ try {
+ const urlObj = new URL(url);
+ return urlObj.hostname;
+ } catch {
+ return url;
+ }
+ }
+
+ /**
+ * Set up console error listeners to detect critical errors
+ */
+ #setupErrorListeners(): void {
+ // Override console.error to capture errors
+ const originalError = console.error;
+ console.error = (...args: unknown[]) => {
+ originalError.apply(console, args);
+ if (this.#isEnabled && this.#currentSite) {
+ this.#handleError(args.map((a) => String(a)).join(" "));
+ }
+ };
+
+ // Listen for unhandled errors
+ window.addEventListener("error", (event) => {
+ if (this.#isEnabled && this.#currentSite) {
+ this.#handleError(event.message || String(event.error));
+ }
+ });
+
+ // Listen for unhandled promise rejections
+ window.addEventListener("unhandledrejection", (event) => {
+ if (this.#isEnabled && this.#currentSite) {
+ this.#handleError(String(event.reason));
+ }
+ });
+ }
+
+ /**
+ * Check if an error message matches critical error patterns
+ */
+ #isCriticalError(errorMessage: string): boolean {
+ // First check if it should be ignored
+ for (const pattern of IGNORED_ERROR_PATTERNS) {
+ if (pattern.test(errorMessage)) {
+ return false;
+ }
+ }
+
+ // Then check if it's a critical error
+ for (const pattern of CRITICAL_ERROR_PATTERNS) {
+ if (pattern.test(errorMessage)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Handle an error and decide whether to switch configurations
+ */
+ #handleError(errorMessage: string): void {
+ if (!this.#isCriticalError(errorMessage)) {
+ return;
+ }
+
+ this.#errorBuffer.push(errorMessage);
+
+ // Debounce error checking
+ if (this.#errorCheckTimeout !== null) {
+ clearTimeout(this.#errorCheckTimeout);
+ }
+
+ this.#errorCheckTimeout = window.setTimeout(() => {
+ this.#processErrorBuffer();
+ }, DynamicLoading.DEBOUNCE_TIME);
+ }
+
+ /**
+ * Process collected errors and decide whether to switch configs
+ */
+ async #processErrorBuffer(): Promise {
+ if (this.#errorBuffer.length >= DynamicLoading.ERROR_THRESHOLD) {
+ console.log(
+ `[DynamicLoading] Detected ${this.#errorBuffer.length} critical errors, attempting config switch`
+ );
+ await this.#tryNextConfig();
+ }
+ this.#errorBuffer = [];
+ this.#errorCheckTimeout = null;
+ }
+
+ /**
+ * Try the next proxy configuration
+ */
+ async #tryNextConfig(): Promise {
+ this.#configIndex++;
+
+ if (this.#configIndex >= PROXY_CONFIGS.length) {
+ // All configs exhausted, redirect to 404
+ console.log("[DynamicLoading] All configurations exhausted, redirecting to 404");
+ this.#redirectTo404();
+ return false;
+ }
+
+ const config = PROXY_CONFIGS[this.#configIndex];
+ console.log(
+ `[DynamicLoading] Switching to config ${this.#configIndex}: proxy=${config.proxy}, transport=${config.transport}, routingMode=${config.routingMode}`
+ );
+
+ await this.#applyConfig(config);
+
+ // Store the working config index for this site
+ if (this.#currentSite) {
+ const domain = this.#extractDomain(this.#currentSite);
+ this.#siteOverrides.set(domain, this.#configIndex);
+ this.#saveSiteOverrides();
+ }
+
+ return true;
+ }
+
+ /**
+ * Apply a proxy configuration
+ */
+ async #applyConfig(config: ProxyConfig): Promise {
+ if (!this.#baremuxConn) {
+ console.warn("[DynamicLoading] BareMux connection not initialized");
+ return;
+ }
+
+ const wispServer = (): string => {
+ const wispServerVal = this.#storageManager.getVal("wispServer");
+ if (this.#storageManager.getVal("adBlock") === "true") {
+ return wispServerVal.replace("/wisp/", "/adblock/");
+ }
+ return wispServerVal;
+ };
+
+ const bareServer = (): string => {
+ return (
+ (location.protocol === "https:" ? "https://" : "http://") + location.host + "/bare/"
+ );
+ };
+
+ // Set the transport based on routing mode
+ if (config.routingMode === "bare") {
+ await this.#baremuxConn.setTransport("/baremod/index.mjs", [bareServer()]);
+ } else {
+ const transportPath =
+ config.transport === "libcurl" ? "/libcurl/index.mjs" : "/epoxy/index.mjs";
+ await this.#baremuxConn.setTransport(transportPath, [{ wisp: wispServer() }]);
+ }
+
+ // Store current config in session for proxy selection
+ sessionStorage.setItem("dynamicLoading_proxy", config.proxy);
+ sessionStorage.setItem("dynamicLoading_transport", config.transport);
+ sessionStorage.setItem("dynamicLoading_routingMode", config.routingMode);
+ }
+
+ /**
+ * Redirect to the 404 page
+ */
+ #redirectTo404(): void {
+ // Clear current site tracking
+ this.#currentSite = null;
+ this.#configIndex = 0;
+ this.#errorCount = 0;
+
+ // Navigate to 404 page
+ window.location.href = "/404";
+ }
+
+ /**
+ * Called when navigating to a new site - stores original config and applies any overrides
+ */
+ async onSiteEnter(siteUrl: string): Promise<"uv" | "sj"> {
+ if (!this.#isEnabled) {
+ return (this.#storageManager.getVal("proxy") as "uv" | "sj") || "uv";
+ }
+
+ this.#currentSite = siteUrl;
+ this.#errorCount = 0;
+ this.#errorBuffer = [];
+
+ // Store original config for later restoration
+ this.#originalConfig = this.#getCurrentConfig();
+
+ // Check if we have a stored override for this site
+ const domain = this.#extractDomain(siteUrl);
+ const overrideIndex = this.#siteOverrides.get(domain);
+
+ if (overrideIndex !== undefined && overrideIndex < PROXY_CONFIGS.length) {
+ // Apply the known working config for this site
+ this.#configIndex = overrideIndex;
+ const config = PROXY_CONFIGS[overrideIndex];
+ console.log(
+ `[DynamicLoading] Applying saved config for ${domain}: proxy=${config.proxy}`
+ );
+ await this.#applyConfig(config);
+ return config.proxy;
+ }
+
+ // Start with first config (index 0)
+ this.#configIndex = 0;
+ return this.#originalConfig.proxy;
+ }
+
+ /**
+ * Called when leaving a site - restores original configuration
+ */
+ async onSiteLeave(): Promise {
+ if (!this.#isEnabled || !this.#originalConfig) {
+ return;
+ }
+
+ console.log("[DynamicLoading] Restoring original configuration");
+
+ // Clear session storage
+ sessionStorage.removeItem("dynamicLoading_proxy");
+ sessionStorage.removeItem("dynamicLoading_transport");
+ sessionStorage.removeItem("dynamicLoading_routingMode");
+
+ // Restore original config
+ await this.#applyConfig(this.#originalConfig);
+
+ // Reset state
+ this.#currentSite = null;
+ this.#errorCount = 0;
+ this.#configIndex = 0;
+ this.#originalConfig = null;
+ this.#errorBuffer = [];
+ }
+
+ /**
+ * Get the currently active proxy type (considering dynamic overrides)
+ */
+ getActiveProxy(): "uv" | "sj" {
+ const override = sessionStorage.getItem("dynamicLoading_proxy");
+ if (override === "uv" || override === "sj") {
+ return override;
+ }
+ return (this.#storageManager.getVal("proxy") as "uv" | "sj") || "uv";
+ }
+
+ /**
+ * Manually trigger a configuration switch (can be called if user detects issues)
+ */
+ async manualSwitch(): Promise {
+ if (!this.#isEnabled || !this.#currentSite) {
+ return false;
+ }
+ return await this.#tryNextConfig();
+ }
+
+ /**
+ * Clear all site-specific overrides
+ */
+ clearOverrides(): void {
+ this.#siteOverrides.clear();
+ this.#storageManager.removeVal("dynamicLoadingOverrides");
+ }
+
+ /**
+ * Get the number of saved site overrides
+ */
+ getOverrideCount(): number {
+ return this.#siteOverrides.size;
+ }
+}
+
+// Singleton instance
+let dynamicLoadingInstance: DynamicLoading | null = null;
+
+/**
+ * Get or create the DynamicLoading singleton instance
+ */
+function getDynamicLoading(): DynamicLoading {
+ if (!dynamicLoadingInstance) {
+ dynamicLoadingInstance = new DynamicLoading();
+ }
+ return dynamicLoadingInstance;
+}
+
+export { DynamicLoading, getDynamicLoading, type ProxyConfig };
diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts
index 6fd13d4..390c28f 100644
--- a/src/utils/proxy.ts
+++ b/src/utils/proxy.ts
@@ -1,6 +1,7 @@
import { BareMuxConnection } from "@mercuryworkshop/bare-mux";
import { StoreManager } from "./storage";
import { initializeCaptchaHandlers } from "./captcha-handler";
+import { getDynamicLoading } from "./dynamic-loading";
const createScript = (src: string, defer?: boolean) => {
const script = document.createElement("script") as HTMLScriptElement;
@@ -46,6 +47,13 @@ class SW {
return this.#ready;
}
+ /**
+ * Get the BareMux connection instance for dynamic loading
+ */
+ getBareMuxConnection(): BareMuxConnection | undefined {
+ return this.#baremuxConn;
+ }
+
search(input: string, template: string) {
try {
return new URL(input).toString();
@@ -60,7 +68,11 @@ class SW {
}
encodeURL(string: string): string {
- const proxy = this.#storageManager.getVal("proxy") as "uv" | "sj";
+ // Check if dynamic loading has an active override
+ const dynamicLoading = getDynamicLoading();
+ const proxy = dynamicLoading.isEnabled()
+ ? dynamicLoading.getActiveProxy()
+ : (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)}`
@@ -159,6 +171,10 @@ class SW {
this.#baremuxConn = new BareMuxConnection("/erab/worker.js");
await this.setTransport();
+ // Initialize dynamic loading with BareMux connection
+ const dynamicLoading = getDynamicLoading();
+ await dynamicLoading.init(this.#baremuxConn);
+
// Load the ScramjetController class from the factory function
const { ScramjetController } = $scramjetLoadController();
From e41297ac97ce27c4c42f0b8dfff802a758bf6679 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 27 Nov 2025 14:29:14 +0000
Subject: [PATCH 3/3] Address code review comments - improve error handling and
cleanup
Co-authored-by: sriail <225764385+sriail@users.noreply.github.com>
---
src/pages/index.astro | 11 +++--
src/pages/settings/experiments.astro | 5 ++-
src/utils/dynamic-loading.ts | 63 ++++++++++++++++++++++++----
3 files changed, 65 insertions(+), 14 deletions(-)
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 1110388..dfaffb0 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -154,12 +154,15 @@ const link = Astro.url.searchParams.get("redir");
phl.classList.remove("hidden");
});
- // Handle page unload to restore config
- window.addEventListener("beforeunload", async () => {
+ // Handle page unload to restore config (synchronous cleanup)
+ // Use both beforeunload and pagehide for better cross-browser support
+ const cleanupHandler = () => {
if (currentSiteUrl && dynamicLoading.isEnabled()) {
- await dynamicLoading.onSiteLeave();
+ dynamicLoading.cleanupSync();
}
- });
+ };
+ window.addEventListener("beforeunload", cleanupHandler);
+ window.addEventListener("pagehide", cleanupHandler);
}
class CustomComponent extends HTMLElement {
diff --git a/src/pages/settings/experiments.astro b/src/pages/settings/experiments.astro
index 9fc48eb..85dfa97 100644
--- a/src/pages/settings/experiments.astro
+++ b/src/pages/settings/experiments.astro
@@ -42,9 +42,12 @@ import { Icon } from "astro-icon/components";
}
// Listen for changes
- dynamicLoadingSwitch.addEventListener("switch-change", (e: Event) => {
+ dynamicLoadingSwitch.addEventListener("switch-change", async (e: Event) => {
const customEvent = e as CustomEvent<{ checked: boolean }>;
storage.setVal("experimentDynamicLoading", customEvent.detail.checked.toString());
+ // Sync the state with the DynamicLoading instance
+ const { getDynamicLoading } = await import("@utils/dynamic-loading");
+ getDynamicLoading().setEnabled(customEvent.detail.checked);
});
};
diff --git a/src/utils/dynamic-loading.ts b/src/utils/dynamic-loading.ts
index 47e497e..6a76a60 100644
--- a/src/utils/dynamic-loading.ts
+++ b/src/utils/dynamic-loading.ts
@@ -114,11 +114,11 @@ class DynamicLoading {
#errorBuffer: string[] = [];
#errorCheckTimeout: number | null = null;
#siteOverrides: Map = new Map(); // Maps site domain to config index
+ #originalConsoleError: ((...args: unknown[]) => void) | null = null;
+ #errorListenersAttached: boolean = false;
// Error threshold before switching configs
static readonly ERROR_THRESHOLD = 3;
- // Time window to collect errors (ms)
- static readonly ERROR_WINDOW = 2000;
// Debounce time for error checking (ms)
static readonly DEBOUNCE_TIME = 500;
@@ -175,7 +175,14 @@ class DynamicLoading {
const stored = this.#storageManager.getVal("dynamicLoadingOverrides");
if (stored) {
const parsed = JSON.parse(stored);
- this.#siteOverrides = new Map(Object.entries(parsed));
+ this.#siteOverrides = new Map(
+ Object.entries(parsed)
+ .map(([k, v]) => {
+ const num = parseInt(String(v), 10);
+ return [k, Number.isNaN(num) ? 0 : num];
+ })
+ .filter(([, v]) => typeof v === "number") as [string, number][]
+ );
}
} catch {
this.#siteOverrides = new Map();
@@ -206,12 +213,19 @@ class DynamicLoading {
* Set up console error listeners to detect critical errors
*/
#setupErrorListeners(): void {
- // Override console.error to capture errors
- const originalError = console.error;
- console.error = (...args: unknown[]) => {
- originalError.apply(console, args);
- if (this.#isEnabled && this.#currentSite) {
- this.#handleError(args.map((a) => String(a)).join(" "));
+ // Only attach listeners once
+ if (this.#errorListenersAttached) {
+ return;
+ }
+ this.#errorListenersAttached = true;
+
+ // Store and override console.error to capture errors
+ this.#originalConsoleError = console.error;
+ const self = this;
+ console.error = function (...args: unknown[]) {
+ self.#originalConsoleError!.apply(console, args);
+ if (self.#isEnabled && self.#currentSite) {
+ self.#handleError(args.map((a) => String(a)).join(" "));
}
};
@@ -230,6 +244,16 @@ class DynamicLoading {
});
}
+ /**
+ * Restore original console.error method
+ */
+ restoreConsoleError(): void {
+ if (this.#originalConsoleError) {
+ console.error = this.#originalConsoleError;
+ this.#originalConsoleError = null;
+ }
+ }
+
/**
* Check if an error message matches critical error patterns
*/
@@ -420,6 +444,27 @@ class DynamicLoading {
await this.#applyConfig(this.#originalConfig);
// Reset state
+ this.#resetState();
+ }
+
+ /**
+ * Synchronous cleanup for use in beforeunload/pagehide handlers
+ * Clears session storage without async operations
+ */
+ cleanupSync(): void {
+ if (!this.#isEnabled) {
+ return;
+ }
+ sessionStorage.removeItem("dynamicLoading_proxy");
+ sessionStorage.removeItem("dynamicLoading_transport");
+ sessionStorage.removeItem("dynamicLoading_routingMode");
+ this.#resetState();
+ }
+
+ /**
+ * Internal state reset helper
+ */
+ #resetState(): void {
this.#currentSite = null;
this.#errorCount = 0;
this.#configIndex = 0;