mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
refactor(webtorrent): extract pure + SW-controller helpers (9/10)
File-size decomposition of the playback-critical module (no behavior change). Move the pure helpers (compareServiceWorkerScripts, normalizeTrackerList, appendProbeTrackers, normalizeNumber, toError) and the service-worker controller-claim pair (requestClientsClaim, waitForActiveController, as instance-param functions with thin delegators) into js/webtorrentHelpers.js. Dropped the now-unused safeDecodeURIComponent import from webtorrent.js. webtorrent.js: 1256 -> 1043 lines (back under its 1087 limit). All moved functions were module-private (no public-export/test-import changes). Build + full unit suite green (incl. WebTorrent regression/handlers/minimal tests). Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
573c9b00ef
commit
370cc67cc5
2 files changed
+250
-224
No files matched your search
+11
-224
@@ -20,7 +20,15 @@
|
||||
|
||||
import WebTorrent from "./webtorrent.min.js";
|
||||
import { WSS_TRACKERS } from "./constants.js";
|
||||
import { safeDecodeURIComponent } from "./utils/safeDecode.js";
|
||||
import {
|
||||
compareServiceWorkerScripts,
|
||||
normalizeTrackerList,
|
||||
appendProbeTrackers,
|
||||
normalizeNumber,
|
||||
toError,
|
||||
requestClientsClaim as requestClientsClaimHelper,
|
||||
waitForActiveController as waitForActiveControllerHelper,
|
||||
} from "./webtorrentHelpers.js";
|
||||
import { devLogger, userLogger } from "./utils/logger.js";
|
||||
import { emit } from "./embedDiagnostics.js";
|
||||
|
||||
@@ -34,136 +42,6 @@ const TORRENT_PLAYBACK_STALL_MS = 12000;
|
||||
// multi-file torrent (largest match wins) and to deselect the rest.
|
||||
const PLAYABLE_VIDEO_PATTERN = /\.(mp4|m4v|webm|mkv|mov|ogv|ogg)$/i;
|
||||
|
||||
function compareServiceWorkerScripts(a = "", b = "") {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const aUrl = new URL(a, window.location.origin);
|
||||
const bUrl = new URL(b, window.location.origin);
|
||||
return aUrl.pathname === bUrl.pathname;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeTrackerList(trackers) {
|
||||
const normalized = [];
|
||||
const seen = new Set();
|
||||
if (!Array.isArray(trackers)) {
|
||||
return normalized;
|
||||
}
|
||||
trackers.forEach((tracker) => {
|
||||
if (typeof tracker !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = tracker.trim();
|
||||
if (!trimmed || !/^wss:\/\//i.test(trimmed)) {
|
||||
return;
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (seen.has(lower)) {
|
||||
return;
|
||||
}
|
||||
seen.add(lower);
|
||||
normalized.push(trimmed);
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function appendProbeTrackers(magnetURI, trackers) {
|
||||
if (typeof magnetURI !== "string") {
|
||||
return { magnet: "", appended: false, hasProbeTrackers: false };
|
||||
}
|
||||
|
||||
const trimmedMagnet = magnetURI.trim();
|
||||
if (!trimmedMagnet) {
|
||||
return { magnet: "", appended: false, hasProbeTrackers: false };
|
||||
}
|
||||
|
||||
const probeTrackers = normalizeTrackerList(trackers);
|
||||
if (!probeTrackers.length) {
|
||||
return {
|
||||
magnet: trimmedMagnet,
|
||||
appended: false,
|
||||
hasProbeTrackers: false,
|
||||
};
|
||||
}
|
||||
|
||||
const trackerSet = new Set();
|
||||
const [withoutFragment, fragment = ""] = trimmedMagnet.split("#", 2);
|
||||
const [, queryPart = ""] = withoutFragment.split("?", 2);
|
||||
|
||||
if (queryPart) {
|
||||
queryPart
|
||||
.split("&")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((segment) => {
|
||||
const [rawKey, rawValue = ""] = segment.split("=", 2);
|
||||
if (!rawKey || rawKey.trim().toLowerCase() !== "tr") {
|
||||
return;
|
||||
}
|
||||
const decoded = safeDecodeURIComponent(rawValue).trim().toLowerCase();
|
||||
if (decoded) {
|
||||
trackerSet.add(decoded);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedProbe = probeTrackers.map((url) => url.toLowerCase());
|
||||
const hadProbeTracker = normalizedProbe.some((url) => trackerSet.has(url));
|
||||
|
||||
const toAppend = [];
|
||||
probeTrackers.forEach((tracker, index) => {
|
||||
const normalizedTracker = normalizedProbe[index];
|
||||
if (trackerSet.has(normalizedTracker)) {
|
||||
return;
|
||||
}
|
||||
trackerSet.add(normalizedTracker);
|
||||
toAppend.push(`tr=${encodeURIComponent(tracker)}`);
|
||||
});
|
||||
|
||||
if (!toAppend.length) {
|
||||
return {
|
||||
magnet: trimmedMagnet,
|
||||
appended: false,
|
||||
hasProbeTrackers: hadProbeTracker,
|
||||
};
|
||||
}
|
||||
|
||||
const separator = queryPart ? "&" : "?";
|
||||
const augmented = `${withoutFragment}${separator}${toAppend.join("&")}`;
|
||||
const finalMagnet = fragment
|
||||
? `${augmented}#${fragment}`
|
||||
: augmented;
|
||||
|
||||
return {
|
||||
magnet: finalMagnet,
|
||||
appended: true,
|
||||
hasProbeTrackers: true,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeNumber(value, fallback = 0) {
|
||||
const coerced = Number(value);
|
||||
if (Number.isFinite(coerced)) {
|
||||
return coerced;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
function toError(err) {
|
||||
if (err instanceof Error) {
|
||||
return err;
|
||||
}
|
||||
try {
|
||||
return new Error(String(err));
|
||||
} catch (stringifyError) {
|
||||
return new Error("Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
export class TorrentClient {
|
||||
constructor({ webTorrentClass } = {}) {
|
||||
@@ -543,102 +421,11 @@ export class TorrentClient {
|
||||
* guard — wait for `controllerchange` whenever `controller` is still null.
|
||||
*/
|
||||
requestClientsClaim(registration = this.swRegistration) {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeWorker = registration?.active;
|
||||
if (!activeWorker) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Some Chromium builds require an explicit startMessages() call before a
|
||||
// yet-to-claim worker will accept postMessage traffic. Calling it is a
|
||||
// harmless no-op elsewhere.
|
||||
if (navigator.serviceWorker.startMessages) {
|
||||
navigator.serviceWorker.startMessages();
|
||||
}
|
||||
|
||||
activeWorker.postMessage({ type: "ENSURE_CLIENTS_CLAIM" });
|
||||
} catch (err) {
|
||||
this.log("Failed to request clients.claim():", err);
|
||||
}
|
||||
return requestClientsClaimHelper(this, registration);
|
||||
}
|
||||
|
||||
async waitForActiveController(registration = this.swRegistration) {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (navigator.serviceWorker.controller) {
|
||||
return navigator.serviceWorker.controller;
|
||||
}
|
||||
|
||||
this.requestClientsClaim(registration);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId = null;
|
||||
let pollId = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
if (pollId) {
|
||||
clearInterval(pollId);
|
||||
pollId = null;
|
||||
}
|
||||
navigator.serviceWorker.removeEventListener(
|
||||
"controllerchange",
|
||||
onControllerChange
|
||||
);
|
||||
};
|
||||
|
||||
const maybeResolve = () => {
|
||||
const controller = navigator.serviceWorker.controller;
|
||||
if (controller) {
|
||||
cleanup();
|
||||
resolve(controller);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onControllerChange = () => {
|
||||
if (maybeResolve()) {
|
||||
return;
|
||||
}
|
||||
// If we received a controllerchange event but still don't have a
|
||||
// controller it usually means the new worker hasn't claimed this page
|
||||
// yet. Ask it again to be safe.
|
||||
this.requestClientsClaim(registration);
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error("Service worker controller claim timeout"));
|
||||
}, this.TIMEOUT_DURATION);
|
||||
|
||||
pollId = setInterval(() => {
|
||||
if (maybeResolve()) {
|
||||
return;
|
||||
}
|
||||
this.requestClientsClaim(registration);
|
||||
}, 500);
|
||||
|
||||
navigator.serviceWorker.addEventListener(
|
||||
"controllerchange",
|
||||
onControllerChange
|
||||
);
|
||||
|
||||
// One last check in case the controller appeared between the earlier
|
||||
// synchronous guard and the promise wiring above.
|
||||
if (!maybeResolve()) {
|
||||
this.requestClientsClaim(registration);
|
||||
}
|
||||
});
|
||||
return waitForActiveControllerHelper(this, registration);
|
||||
}
|
||||
|
||||
async setupServiceWorker() {
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
// js/webtorrentHelpers.js
|
||||
//
|
||||
// Helpers extracted from webtorrent.js to keep that module under the file-size
|
||||
// budget. The first group is pure (no instance state). requestClientsClaim and
|
||||
// waitForActiveController take the TorrentClient instance (`ctx`) so behavior is
|
||||
// identical to the former methods; the client keeps thin delegators so all call
|
||||
// sites are unchanged. No behavior change.
|
||||
|
||||
import { safeDecodeURIComponent } from "./utils/safeDecode.js";
|
||||
|
||||
export function compareServiceWorkerScripts(a = "", b = "") {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
const aUrl = new URL(a, window.location.origin);
|
||||
const bUrl = new URL(b, window.location.origin);
|
||||
return aUrl.pathname === bUrl.pathname;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function normalizeTrackerList(trackers) {
|
||||
const normalized = [];
|
||||
const seen = new Set();
|
||||
if (!Array.isArray(trackers)) {
|
||||
return normalized;
|
||||
}
|
||||
trackers.forEach((tracker) => {
|
||||
if (typeof tracker !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = tracker.trim();
|
||||
if (!trimmed || !/^wss:\/\//i.test(trimmed)) {
|
||||
return;
|
||||
}
|
||||
const lower = trimmed.toLowerCase();
|
||||
if (seen.has(lower)) {
|
||||
return;
|
||||
}
|
||||
seen.add(lower);
|
||||
normalized.push(trimmed);
|
||||
});
|
||||
return normalized;
|
||||
}
|
||||
|
||||
export function appendProbeTrackers(magnetURI, trackers) {
|
||||
if (typeof magnetURI !== "string") {
|
||||
return { magnet: "", appended: false, hasProbeTrackers: false };
|
||||
}
|
||||
|
||||
const trimmedMagnet = magnetURI.trim();
|
||||
if (!trimmedMagnet) {
|
||||
return { magnet: "", appended: false, hasProbeTrackers: false };
|
||||
}
|
||||
|
||||
const probeTrackers = normalizeTrackerList(trackers);
|
||||
if (!probeTrackers.length) {
|
||||
return {
|
||||
magnet: trimmedMagnet,
|
||||
appended: false,
|
||||
hasProbeTrackers: false,
|
||||
};
|
||||
}
|
||||
|
||||
const trackerSet = new Set();
|
||||
const [withoutFragment, fragment = ""] = trimmedMagnet.split("#", 2);
|
||||
const [, queryPart = ""] = withoutFragment.split("?", 2);
|
||||
|
||||
if (queryPart) {
|
||||
queryPart
|
||||
.split("&")
|
||||
.map((segment) => segment.trim())
|
||||
.filter(Boolean)
|
||||
.forEach((segment) => {
|
||||
const [rawKey, rawValue = ""] = segment.split("=", 2);
|
||||
if (!rawKey || rawKey.trim().toLowerCase() !== "tr") {
|
||||
return;
|
||||
}
|
||||
const decoded = safeDecodeURIComponent(rawValue).trim().toLowerCase();
|
||||
if (decoded) {
|
||||
trackerSet.add(decoded);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const normalizedProbe = probeTrackers.map((url) => url.toLowerCase());
|
||||
const hadProbeTracker = normalizedProbe.some((url) => trackerSet.has(url));
|
||||
|
||||
const toAppend = [];
|
||||
probeTrackers.forEach((tracker, index) => {
|
||||
const normalizedTracker = normalizedProbe[index];
|
||||
if (trackerSet.has(normalizedTracker)) {
|
||||
return;
|
||||
}
|
||||
trackerSet.add(normalizedTracker);
|
||||
toAppend.push(`tr=${encodeURIComponent(tracker)}`);
|
||||
});
|
||||
|
||||
if (!toAppend.length) {
|
||||
return {
|
||||
magnet: trimmedMagnet,
|
||||
appended: false,
|
||||
hasProbeTrackers: hadProbeTracker,
|
||||
};
|
||||
}
|
||||
|
||||
const separator = queryPart ? "&" : "?";
|
||||
const augmented = `${withoutFragment}${separator}${toAppend.join("&")}`;
|
||||
const finalMagnet = fragment
|
||||
? `${augmented}#${fragment}`
|
||||
: augmented;
|
||||
|
||||
return {
|
||||
magnet: finalMagnet,
|
||||
appended: true,
|
||||
hasProbeTrackers: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeNumber(value, fallback = 0) {
|
||||
const coerced = Number(value);
|
||||
if (Number.isFinite(coerced)) {
|
||||
return coerced;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export function toError(err) {
|
||||
if (err instanceof Error) {
|
||||
return err;
|
||||
}
|
||||
try {
|
||||
return new Error(String(err));
|
||||
} catch (stringifyError) {
|
||||
return new Error("Unknown error");
|
||||
}
|
||||
}
|
||||
|
||||
export function requestClientsClaim(ctx, registration = ctx.swRegistration) {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const activeWorker = registration?.active;
|
||||
if (!activeWorker) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Some Chromium builds require an explicit startMessages() call before a
|
||||
// yet-to-claim worker will accept postMessage traffic. Calling it is a
|
||||
// harmless no-op elsewhere.
|
||||
if (navigator.serviceWorker.startMessages) {
|
||||
navigator.serviceWorker.startMessages();
|
||||
}
|
||||
|
||||
activeWorker.postMessage({ type: "ENSURE_CLIENTS_CLAIM" });
|
||||
} catch (err) {
|
||||
ctx.log("Failed to request clients.claim():", err);
|
||||
}
|
||||
}
|
||||
|
||||
export async function waitForActiveController(ctx, registration = ctx.swRegistration) {
|
||||
if (!("serviceWorker" in navigator)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (navigator.serviceWorker.controller) {
|
||||
return navigator.serviceWorker.controller;
|
||||
}
|
||||
|
||||
ctx.requestClientsClaim(registration);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let timeoutId = null;
|
||||
let pollId = null;
|
||||
|
||||
const cleanup = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = null;
|
||||
}
|
||||
if (pollId) {
|
||||
clearInterval(pollId);
|
||||
pollId = null;
|
||||
}
|
||||
navigator.serviceWorker.removeEventListener(
|
||||
"controllerchange",
|
||||
onControllerChange
|
||||
);
|
||||
};
|
||||
|
||||
const maybeResolve = () => {
|
||||
const controller = navigator.serviceWorker.controller;
|
||||
if (controller) {
|
||||
cleanup();
|
||||
resolve(controller);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const onControllerChange = () => {
|
||||
if (maybeResolve()) {
|
||||
return;
|
||||
}
|
||||
// If we received a controllerchange event but still don't have a
|
||||
// controller it usually means the new worker hasn't claimed this page
|
||||
// yet. Ask it again to be safe.
|
||||
ctx.requestClientsClaim(registration);
|
||||
};
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
cleanup();
|
||||
reject(new Error("Service worker controller claim timeout"));
|
||||
}, ctx.TIMEOUT_DURATION);
|
||||
|
||||
pollId = setInterval(() => {
|
||||
if (maybeResolve()) {
|
||||
return;
|
||||
}
|
||||
ctx.requestClientsClaim(registration);
|
||||
}, 500);
|
||||
|
||||
navigator.serviceWorker.addEventListener(
|
||||
"controllerchange",
|
||||
onControllerChange
|
||||
);
|
||||
|
||||
// One last check in case the controller appeared between the earlier
|
||||
// synchronous guard and the promise wiring above.
|
||||
if (!maybeResolve()) {
|
||||
ctx.requestClientsClaim(registration);
|
||||
}
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user