From 78ce75ef62fd2159ad4aead797ead644e4d9408e Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 15 Oct 2025 14:54:24 -0400 Subject: [PATCH 1/2] Handle WebTorrent fallback without service worker --- js/app.js | 19 +++++ js/utils/serviceWorkerFallbackMessages.js | 58 +++++++++++++ js/webtorrent.js | 82 +++++++++++++++---- .../service-worker-fallback-message.test.mjs | 57 +++++++++++++ 4 files changed, 198 insertions(+), 18 deletions(-) create mode 100644 js/utils/serviceWorkerFallbackMessages.js create mode 100644 tests/torrent/service-worker-fallback-message.test.mjs diff --git a/js/app.js b/js/app.js index 090d06e7..069c29ee 100644 --- a/js/app.js +++ b/js/app.js @@ -81,6 +81,7 @@ import { pointerArrayToKey } from "./utils/pointer.js"; import { resolveVideoPointer } from "./utils/videoPointer.js"; import { isValidMagnetUri } from "./utils/magnetValidators.js"; import { dedupeToNewestByRoot } from "./utils/videoDeduper.js"; +import { buildServiceWorkerFallbackStatus } from "./utils/serviceWorkerFallbackMessages.js"; import { getVideoRootIdentifier, applyRootTimestampToVideosMap, @@ -5635,6 +5636,24 @@ class Application { this.modalVideo, { urlList: sanitizedUrlList } ); + + if (torrentClient.isServiceWorkerUnavailable()) { + const swError = torrentClient.getServiceWorkerInitError(); + const statusMessage = buildServiceWorkerFallbackStatus(swError); + this.log( + "[playViaWebTorrent] Service worker unavailable; streaming directly via WebTorrent.", + swError + ); + if (swError) { + userLogger.warn( + "[playViaWebTorrent] Service worker unavailable; direct streaming engaged.", + swError + ); + } + if (this.videoModal) { + this.videoModal.updateStatus(statusMessage); + } + } if (torrentInstance && torrentInstance.ready) { // Some browsers delay `playing` events for MediaSource-backed torrents. // Clearing the poster here prevents the historic "GIF stuck over the diff --git a/js/utils/serviceWorkerFallbackMessages.js b/js/utils/serviceWorkerFallbackMessages.js new file mode 100644 index 00000000..6110b92c --- /dev/null +++ b/js/utils/serviceWorkerFallbackMessages.js @@ -0,0 +1,58 @@ +// js/utils/serviceWorkerFallbackMessages.js + +const BASE_STATUS_MESSAGE = "Streaming via WebTorrent"; + +function normalizeMessage(error) { + if (!error) { + return ""; + } + if (error instanceof Error && typeof error.message === "string") { + return error.message; + } + if (typeof error === "string") { + return error; + } + try { + return String(error); + } catch (_err) { + return ""; + } +} + +export function buildServiceWorkerFallbackStatus(error) { + const rawMessage = normalizeMessage(error); + const normalized = rawMessage.toLowerCase(); + + if (!normalized) { + return `${BASE_STATUS_MESSAGE} (service worker unavailable)`; + } + + if (normalized.includes("https or localhost required")) { + return `${BASE_STATUS_MESSAGE} (serve over HTTPS to enable service worker)`; + } + + if (normalized.includes("not supported") || normalized.includes("disabled")) { + return `${BASE_STATUS_MESSAGE} (browser disabled service workers)`; + } + + if (normalized.includes("brave shield")) { + return `${BASE_STATUS_MESSAGE} (Brave Shields blocked the service worker)`; + } + + if ( + normalized.includes("failed to register") || + normalized.includes("failed to fetch") || + normalized.includes("blocked") || + normalized.includes("net::err_blocked_by_client") + ) { + return `${BASE_STATUS_MESSAGE} (service worker blocked by browser or extension)`; + } + + if (normalized.includes("controller claim timeout")) { + return `${BASE_STATUS_MESSAGE} (waiting for service worker control)`; + } + + return `${BASE_STATUS_MESSAGE} (service worker unavailable)`; +} + +export default buildServiceWorkerFallbackStatus; diff --git a/js/webtorrent.js b/js/webtorrent.js index 35315765..25efa6f5 100644 --- a/js/webtorrent.js +++ b/js/webtorrent.js @@ -125,7 +125,9 @@ function toError(err) { } export class TorrentClient { - constructor() { + constructor({ webTorrentClass } = {}) { + this.WebTorrentClass = + typeof webTorrentClass === "function" ? webTorrentClass : WebTorrent; // Reusable objects and flags this.client = null; this.currentTorrent = null; @@ -135,13 +137,16 @@ export class TorrentClient { this.swRegistration = null; this.serverCreated = false; // Indicates if we've called createServer on this.client + this.serviceWorkerDisabled = false; + this.serviceWorkerError = null; + // Timeout for SW operations this.TIMEOUT_DURATION = 60000; } ensureClientForProbe() { if (!this.probeClient) { - this.probeClient = new WebTorrent(); + this.probeClient = new this.WebTorrentClass(); } return this.probeClient; } @@ -288,8 +293,16 @@ export class TorrentClient { }); } - log(msg) { - devLogger.log(msg); + log(...args) { + devLogger.log(...args); + } + + isServiceWorkerUnavailable() { + return this.serviceWorkerDisabled; + } + + getServiceWorkerInitError() { + return this.serviceWorkerError || null; } async isBrave() { @@ -307,21 +320,52 @@ export class TorrentClient { * Called once from streamVideo. */ async init() { - // 1) If the client doesn't exist, create it if (!this.client) { - this.client = new WebTorrent(); + this.client = new this.WebTorrentClass(); } - // 2) If we haven’t registered the service worker yet, do it now - if (!this.swRegistration) { - this.swRegistration = await this.setupServiceWorker(); - } else { - // Even with an existing registration we still wait for control so that a - // transient controller drop (e.g. Chrome devtools unregister/reload) does - // not resurrect the grey-screen regression mentioned in - // waitForActiveController(). - this.requestClientsClaim(this.swRegistration); - await this.waitForActiveController(this.swRegistration); + if (this.serviceWorkerDisabled) { + return { + serviceWorkerReady: false, + registration: null, + error: this.serviceWorkerError, + }; + } + + try { + if (!this.swRegistration) { + this.swRegistration = await this.setupServiceWorker(); + } else { + // Even with an existing registration we still wait for control so that a + // transient controller drop (e.g. Chrome devtools unregister/reload) does + // not resurrect the grey-screen regression mentioned in + // waitForActiveController(). + this.requestClientsClaim(this.swRegistration); + await this.waitForActiveController(this.swRegistration); + } + + return { + serviceWorkerReady: !!this.swRegistration, + registration: this.swRegistration, + }; + } catch (error) { + const normalizedError = toError(error); + this.log( + "[WebTorrent] Service worker setup failed; continuing without it:", + normalizedError + ); + userLogger.warn( + "[WebTorrent] Service worker unavailable; falling back to direct streaming.", + normalizedError + ); + this.serviceWorkerDisabled = true; + this.serviceWorkerError = normalizedError; + this.swRegistration = null; + return { + serviceWorkerReady: false, + registration: null, + error: normalizedError, + }; } } @@ -777,10 +821,12 @@ export class TorrentClient { async streamVideo(magnetURI, videoElement, opts = {}) { try { // 1) Make sure we have a WebTorrent client and a valid SW registration. - await this.init(); + const initResult = await this.init(); + const serviceWorkerReady = + !!(initResult?.serviceWorkerReady && this.swRegistration); // 2) Create the server once if not already created. - if (!this.serverCreated) { + if (serviceWorkerReady && !this.serverCreated) { this.client.createServer({ controller: this.swRegistration, pathPrefix: location.origin + "/webtorrent", diff --git a/tests/torrent/service-worker-fallback-message.test.mjs b/tests/torrent/service-worker-fallback-message.test.mjs new file mode 100644 index 00000000..e8294036 --- /dev/null +++ b/tests/torrent/service-worker-fallback-message.test.mjs @@ -0,0 +1,57 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + buildServiceWorkerFallbackStatus, +} from "../../js/utils/serviceWorkerFallbackMessages.js"; + +const BASE = "Streaming via WebTorrent"; + +test("returns generic message when error missing", () => { + assert.strictEqual( + buildServiceWorkerFallbackStatus(null), + `${BASE} (service worker unavailable)` + ); +}); + +test("identifies HTTPS requirement errors", () => { + const error = new Error("HTTPS or localhost required"); + assert.strictEqual( + buildServiceWorkerFallbackStatus(error), + `${BASE} (serve over HTTPS to enable service worker)` + ); +}); + +test("identifies disabled service worker errors", () => { + const error = new Error("Service Worker not supported or disabled"); + assert.strictEqual( + buildServiceWorkerFallbackStatus(error), + `${BASE} (browser disabled service workers)` + ); +}); + +test("identifies Brave specific guidance", () => { + const error = new Error("Please enable Service Workers in Brave Shield settings"); + assert.strictEqual( + buildServiceWorkerFallbackStatus(error), + `${BASE} (Brave Shields blocked the service worker)` + ); +}); + +test("identifies blocked script errors", () => { + const error = new Error( + "Failed to register ServiceWorker: TypeError: Failed to fetch" + ); + assert.strictEqual( + buildServiceWorkerFallbackStatus(error), + `${BASE} (service worker blocked by browser or extension)` + ); +}); + +test("identifies controller claim timeout", () => { + const error = new Error("Service worker controller claim timeout"); + assert.strictEqual( + buildServiceWorkerFallbackStatus(error), + `${BASE} (waiting for service worker control)` + ); +}); From 0a2a2c4e78c4f7d6fe2e1d4dfc85845ed769c54d Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Wed, 15 Oct 2025 15:08:55 -0400 Subject: [PATCH 2/2] Fix syntax errors blocking app bootstrap --- js/adminListStore.js | 14 +++-- js/channelProfile.js | 2 +- js/config.js | 2 +- js/index.js | 2 +- js/nostr.js | 83 +++++++++++++++------------- js/nostrEventSchemas.js | 7 ++- js/payments/zapSplit.js | 2 +- js/relayManager.js | 2 +- js/services/authService.js | 2 +- js/services/nostrService.js | 2 +- js/services/watchHistoryTelemetry.js | 2 +- js/state/cache.js | 2 +- js/ui/components/VideoCard.js | 2 +- js/ui/moreMenuController.js | 2 +- js/ui/watchHistoryController.js | 12 ++-- js/ui/zapController.js | 2 +- js/userBlocks.js | 2 +- js/viewCounter.js | 2 +- 18 files changed, 80 insertions(+), 64 deletions(-) diff --git a/js/adminListStore.js b/js/adminListStore.js index 5d82cffc..2da0679a 100644 --- a/js/adminListStore.js +++ b/js/adminListStore.js @@ -8,8 +8,8 @@ import { isDevMode, } from "./config.js"; import { nostrClient } from "./nostr.js"; -import { import { devLogger, userLogger } from "./utils/logger.js"; +import { buildAdminListEvent, ADMIN_LIST_IDENTIFIERS, } from "./nostrEventSchemas.js"; @@ -151,8 +151,10 @@ function readJsonListFromStorage(key) { const parsed = JSON.parse(raw); return Array.isArray(parsed) ? parsed : null; } catch (error) { - devLogger.warn(`[adminListStore] Failed to parse legacy list for ${key:`, error); - } + devLogger.warn( + `[adminListStore] Failed to parse legacy list for ${key}:`, + error, + ); return null; } } @@ -198,8 +200,10 @@ function clearLegacyStorageFor(listKey) { try { localStorage.removeItem(key); } catch (error) { - devLogger.warn(`[adminListStore] Failed to clear legacy storage for ${key:`, error); - } + devLogger.warn( + `[adminListStore] Failed to clear legacy storage for ${key}:`, + error, + ); } } } diff --git a/js/channelProfile.js b/js/channelProfile.js index f2b2ec4f..e2c51ee3 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -40,8 +40,8 @@ import { requestInvoice } from "./payments/lnurl.js"; import { getPlatformLightningAddress } from "./payments/platformAddress.js"; -import { import { userLogger } from "./utils/logger.js"; +import { ensureWallet, sendPayment as sendWalletPayment } from "./payments/nwcClient.js"; diff --git a/js/config.js b/js/config.js index 5f4e89a9..77088cbb 100644 --- a/js/config.js +++ b/js/config.js @@ -1,7 +1,7 @@ // js/config.js -import { import { userLogger } from "./utils/logger.js"; +import { IS_DEV_MODE, ADMIN_SUPER_NPUB, ADMIN_DM_IMAGE_URL, diff --git a/js/index.js b/js/index.js index dbf45644..e91a0ab9 100644 --- a/js/index.js +++ b/js/index.js @@ -13,8 +13,8 @@ import { initThemeController, refreshThemeControls, } from "./themeController.js"; -import { import { devLogger, userLogger } from "./utils/logger.js"; +import { prepareStaticModal, openStaticModal, closeStaticModal, diff --git a/js/nostr.js b/js/nostr.js index 63ad0828..e6907127 100644 --- a/js/nostr.js +++ b/js/nostr.js @@ -1620,8 +1620,7 @@ function normalizePointerInput(pointer) { } } } catch (err) { - devLogger.warn(`[nostr] Failed to decode pointer ${trimmed:`, err); - } + devLogger.warn(`[nostr] Failed to decode pointer ${trimmed}:`, err); } } @@ -5681,17 +5680,18 @@ export class NostrClient { this.restoreLocalData(); - try { - await this.ensurePool(); - const results = await this.connectToRelays(); - const successfulRelays = results - .filter((r) => r.success) - .map((r) => r.url); - if (successfulRelays.length === 0) { - throw new Error("No relays connected"); - } - devLogger.log(`Connected to ${successfulRelays.length relay(s)`); - } + try { + await this.ensurePool(); + const results = await this.connectToRelays(); + const successfulRelays = results + .filter((r) => r.success) + .map((r) => r.url); + if (successfulRelays.length === 0) { + throw new Error("No relays connected"); + } + devLogger.log( + `Connected to ${successfulRelays.length} relay(s)`, + ); } catch (err) { userLogger.error("Nostr init failed:", err); throw err; @@ -6159,12 +6159,11 @@ export class NostrClient { } } - if (!signerPubkey && signedEvent && typeof signedEvent.pubkey === "string") { - signerPubkey = signedEvent.pubkey; - } + if (!signerPubkey && signedEvent && typeof signedEvent.pubkey === "string") { + signerPubkey = signedEvent.pubkey; + } - devLogger.log(`Signed ${devLogLabel event:`, signedEvent); - } + devLogger.log(`Signed ${devLogLabel} event:`, signedEvent); let targetRelays = sanitizeRelayList( Array.isArray(relaysOverride) && relaysOverride.length @@ -7373,11 +7372,13 @@ export class NostrClient { try { const events = await this.pool.list([url], [filter]); return Array.isArray(events) ? events : []; - } catch (error) { - devLogger.warn(`[nostr] NIP-71 fetch failed on ${url:`, error); + } catch (error) { + devLogger.warn( + `[nostr] NIP-71 fetch failed on ${url}:`, + error, + ); + return []; } - return []; - } }) ); @@ -8538,14 +8539,18 @@ export class NostrClient { }); const count = this.extractCountValue(frame?.[2]); return { url, ok: true, frame, count }; - } catch (error) { - const isUnsupported = error?.code === "count-unsupported"; - if (isUnsupported) { - this.countUnsupportedRelays.add(url); - } else devLogger.warn(`[nostr] COUNT request failed on ${url:`, error); + } catch (error) { + const isUnsupported = error?.code === "count-unsupported"; + if (isUnsupported) { + this.countUnsupportedRelays.add(url); + } else { + devLogger.warn( + `[nostr] COUNT request failed on ${url}:`, + error, + ); + } + return { url, ok: false, error, unsupported: isUnsupported }; } - return { url, ok: false, error, unsupported: isUnsupported }; - } }) ); @@ -8735,16 +8740,18 @@ export class NostrClient { try { const perRelay = await Promise.all( - this.relays.map(async (url) => { - try { - const events = await this.pool.list([url], [filter]); - return events || []; - } catch (err) { - devLogger.warn(`[nostr] History fetch failed on ${url:`, err); + this.relays.map(async (url) => { + try { + const events = await this.pool.list([url], [filter]); + return events || []; + } catch (err) { + devLogger.warn( + `[nostr] History fetch failed on ${url}:`, + err, + ); + return []; } - return []; - } - }) + }) ); const merged = perRelay.flat(); diff --git a/js/nostrEventSchemas.js b/js/nostrEventSchemas.js index fa0e2460..766f483d 100644 --- a/js/nostrEventSchemas.js +++ b/js/nostrEventSchemas.js @@ -1,5 +1,5 @@ -import { import { devLogger } from "./utils/logger.js"; +import { ADMIN_LIST_NAMESPACE, isDevMode, WATCH_HISTORY_KIND, @@ -830,8 +830,9 @@ function resolveAdminNoteType(listKey) { export function buildAdminListEvent(listKey, { pubkey, created_at, hexPubkeys = [] }) { const schema = getNostrEventSchema(resolveAdminNoteType(listKey)); if (!schema) { - devLogger.warn(`[nostrEventSchemas] Unknown admin list key: ${listKey`); - } + devLogger.warn( + `[nostrEventSchemas] Unknown admin list key: ${listKey}`, + ); return { kind: 30000, pubkey, diff --git a/js/payments/zapSplit.js b/js/payments/zapSplit.js index 98f5e643..21e7bddb 100644 --- a/js/payments/zapSplit.js +++ b/js/payments/zapSplit.js @@ -10,8 +10,8 @@ import { } from "./lnurl.js"; import { ensureWallet, sendPayment } from "./nwcClient.js"; import { getPlatformLightningAddress } from "./platformAddress.js"; -import { import { userLogger } from "../utils/logger.js"; +import { clampPercent, parsePercentValue, resolvePlatformFeePercent, diff --git a/js/relayManager.js b/js/relayManager.js index 668e70f8..f17fe764 100644 --- a/js/relayManager.js +++ b/js/relayManager.js @@ -1,8 +1,8 @@ import { isDevMode } from "./config.js"; import { DEFAULT_RELAY_URLS, nostrClient } from "./nostr.js"; import { buildRelayListEvent } from "./nostrEventSchemas.js"; -import { import { devLogger, userLogger } from "./utils/logger.js"; +import { publishEventToRelays, assertAnyRelayAccepted, } from "./nostrPublish.js"; diff --git a/js/services/authService.js b/js/services/authService.js index 9ec018e0..0667a455 100644 --- a/js/services/authService.js +++ b/js/services/authService.js @@ -6,8 +6,8 @@ import { getPubkey, getCurrentUserNpub, } from "../state/appState.js"; -import { import { userLogger } from "../utils/logger.js"; +import { getSavedProfiles, getActiveProfilePubkey, setActiveProfilePubkey, diff --git a/js/services/nostrService.js b/js/services/nostrService.js index a7784b40..9e76b154 100644 --- a/js/services/nostrService.js +++ b/js/services/nostrService.js @@ -4,8 +4,8 @@ import { } from "../nostr.js"; import { accessControl } from "../accessControl.js"; import { ALLOW_NSFW_CONTENT } from "../config.js"; -import { import { userLogger } from "../utils/logger.js"; +import { getVideosMap as getStoredVideosMap, setVideosMap as setStoredVideosMap, getVideoSubscription as getStoredVideoSubscription, diff --git a/js/services/watchHistoryTelemetry.js b/js/services/watchHistoryTelemetry.js index bd2bad43..14d565f1 100644 --- a/js/services/watchHistoryTelemetry.js +++ b/js/services/watchHistoryTelemetry.js @@ -1,7 +1,7 @@ import { isDevMode } from "../config.js"; import { pointerArrayToKey } from "../utils/pointer.js"; -import { import { devLogger, userLogger } from "../utils/logger.js"; +import { ingestLocalViewEvent as defaultIngestLocalViewEvent, } from "../viewCounter.js"; diff --git a/js/state/cache.js b/js/state/cache.js index f8bc49f6..52332ff6 100644 --- a/js/state/cache.js +++ b/js/state/cache.js @@ -1,5 +1,5 @@ -import { import { userLogger } from "../utils/logger.js"; +import { readUrlHealthFromStorage, removeUrlHealthFromStorage, writeUrlHealthToStorage, diff --git a/js/ui/components/VideoCard.js b/js/ui/components/VideoCard.js index f8a528d4..ee3e8757 100644 --- a/js/ui/components/VideoCard.js +++ b/js/ui/components/VideoCard.js @@ -2,8 +2,8 @@ import { normalizeDesignSystemContext } from "../../designSystem.js"; import { updateVideoCardSourceVisibility } from "../../utils/cardSourceVisibility.js"; import positionFloatingPanel from "../utils/positionFloatingPanel.js"; import { createFloatingPanelStyles } from "../utils/floatingPanelStyles.js"; -import { import { userLogger } from "../../utils/logger.js"; +import { getPopupOffsetPx, getPopupViewportPaddingPx, } from "../../designSystem/metrics.js"; diff --git a/js/ui/moreMenuController.js b/js/ui/moreMenuController.js index d911f493..ef4cd3a2 100644 --- a/js/ui/moreMenuController.js +++ b/js/ui/moreMenuController.js @@ -1,8 +1,8 @@ import { normalizeDesignSystemContext } from "../designSystem.js"; import positionFloatingPanel from "./utils/positionFloatingPanel.js"; import { createFloatingPanelStyles } from "./utils/floatingPanelStyles.js"; -import { import { userLogger } from "../utils/logger.js"; +import { getPopupOffsetPx, getPopupViewportPaddingPx, } from "../designSystem/metrics.js"; diff --git a/js/ui/watchHistoryController.js b/js/ui/watchHistoryController.js index 355e9434..252128c9 100644 --- a/js/ui/watchHistoryController.js +++ b/js/ui/watchHistoryController.js @@ -350,13 +350,17 @@ export default class WatchHistoryController { try { const result = this.watchHistoryService.snapshot(undefined, { reason }); return Promise.resolve(result).catch((error) => { - devLogger.warn(`[${context] Watch history flush failed:`, error); - } + devLogger.warn( + `[${context}] Watch history flush failed:`, + error, + ); throw error; }); } catch (error) { - devLogger.warn(`[${context] Failed to queue watch history flush:`, error); - } + devLogger.warn( + `[${context}] Failed to queue watch history flush:`, + error, + ); return Promise.reject(error); } } diff --git a/js/ui/zapController.js b/js/ui/zapController.js index de87aeb0..e4135f7b 100644 --- a/js/ui/zapController.js +++ b/js/ui/zapController.js @@ -18,8 +18,8 @@ import { requestInvoice, } from "../payments/lnurl.js"; import { getPlatformLightningAddress } from "../payments/platformAddress.js"; -import { import { devLogger, userLogger } from "../utils/logger.js"; +import { ensureWallet as ensureWalletDefault, sendPayment as sendPaymentDefault, } from "../payments/nwcClient.js"; diff --git a/js/userBlocks.js b/js/userBlocks.js index ecae8a3b..57863c4d 100644 --- a/js/userBlocks.js +++ b/js/userBlocks.js @@ -1,8 +1,8 @@ // js/userBlocks.js import { nostrClient } from "./nostr.js"; import { buildBlockListEvent, BLOCK_LIST_IDENTIFIER } from "./nostrEventSchemas.js"; -import { import { userLogger } from "./utils/logger.js"; +import { publishEventToRelays, assertAnyRelayAccepted, } from "./nostrPublish.js"; diff --git a/js/viewCounter.js b/js/viewCounter.js index 406bc766..ea687232 100644 --- a/js/viewCounter.js +++ b/js/viewCounter.js @@ -16,8 +16,8 @@ import { VIEW_COUNT_CACHE_TTL_MS, VIEW_COUNT_DEDUPE_WINDOW_SECONDS, } from "./config.js"; -import { import { userLogger } from "./utils/logger.js"; +import { countVideoViewEvents, listVideoViewEvents, subscribeVideoViewEvents,