mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
fix(beacon): stop the processing spinner hanging forever (#28)
The torrent beacon's processing overlay was gated on WebTorrent's metadata-ready
callback (client.add/seed), which only fires once metadata arrives from peers. A
magnet with no live seeders never resolved it, so the spinner span forever, and a
torrent that emitted its own 'error' didn't always reach the client-level handler.
- beginProcessing() arms a 30s watchdog that drops the overlay and warns ("still
searching for peers — it'll keep trying in the background") instead of hanging;
the torrent stays in the table and keeps announcing.
- handleTorrentReady, the client error handler, and destroy() clear the watchdog.
- addMagnet/seedFiles wrap add/seed in try/catch and attach a per-torrent 'error'
listener so bad/duplicate magnets fail visibly.
- Tests: tests/torrent/beacon-watchdog.test.mjs (JSDOM).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
1c256808ff
commit
fdcf034464
4 files changed
+222
-15
No files matched your search
+78
-8
@@ -12,6 +12,12 @@ const TRACKERS = [
|
||||
const TORRENT_OPTIONS = { announce: TRACKERS };
|
||||
const TRACKER_OPTIONS = { announce: TRACKERS };
|
||||
|
||||
// A magnet with no live seeders never fires WebTorrent's metadata-ready
|
||||
// callback, so the processing overlay would otherwise spin forever. After this
|
||||
// long we drop the overlay and tell the user it's still trying in the
|
||||
// background (the torrent stays in the table and keeps announcing).
|
||||
const PROCESSING_TIMEOUT_MS = 30000;
|
||||
|
||||
function formatBytes(num, speed = false) {
|
||||
if (typeof num !== "number" || Number.isNaN(num)) {
|
||||
return "";
|
||||
@@ -208,11 +214,37 @@ export function createBeaconApp({
|
||||
|
||||
const cleanupTasks = [];
|
||||
const intervalHandles = [];
|
||||
let processingTimeoutId = null;
|
||||
|
||||
function addCleanup(task) {
|
||||
cleanupTasks.push(task);
|
||||
}
|
||||
|
||||
function clearProcessingWatchdog() {
|
||||
if (processingTimeoutId !== null) {
|
||||
view.clearTimeout(processingTimeoutId);
|
||||
processingTimeoutId = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Show the processing overlay and arm a watchdog so it can never hang
|
||||
// forever: if the operation hasn't completed in time, drop the overlay and
|
||||
// warn instead of leaving a dead spinner.
|
||||
function beginProcessing() {
|
||||
setProcessing(true);
|
||||
clearProcessingWatchdog();
|
||||
processingTimeoutId = view.setTimeout(() => {
|
||||
processingTimeoutId = null;
|
||||
if (!state.processing) {
|
||||
return;
|
||||
}
|
||||
setProcessing(false);
|
||||
toast?.warn(
|
||||
"Still searching for peers — this is taking a while. It'll keep trying in the background.",
|
||||
);
|
||||
}, PROCESSING_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
function setProcessing(flag) {
|
||||
state.processing = Boolean(flag);
|
||||
|
||||
@@ -359,6 +391,7 @@ export function createBeaconApp({
|
||||
}
|
||||
|
||||
function handleTorrentReady(torrent, { isSeed = false } = {}) {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
prepareTorrentFiles(torrent);
|
||||
|
||||
@@ -380,10 +413,20 @@ export function createBeaconApp({
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
client.add(trimmed, TORRENT_OPTIONS, (torrent) => {
|
||||
handleTorrentReady(torrent, { isSeed: false });
|
||||
});
|
||||
beginProcessing();
|
||||
let torrent;
|
||||
try {
|
||||
torrent = client.add(trimmed, TORRENT_OPTIONS, (added) => {
|
||||
handleTorrentReady(added, { isSeed: false });
|
||||
});
|
||||
} catch (error) {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
userLogger.error("[beacon] Failed to add magnet", error);
|
||||
toast?.error(error?.message || "Couldn't add that magnet.");
|
||||
return;
|
||||
}
|
||||
watchTorrentForFailure(torrent);
|
||||
}
|
||||
|
||||
function seedFiles(files) {
|
||||
@@ -391,10 +434,35 @@ export function createBeaconApp({
|
||||
return;
|
||||
}
|
||||
|
||||
setProcessing(true);
|
||||
client.seed(files, TORRENT_OPTIONS, (torrent) => {
|
||||
handleTorrentReady(torrent, { isSeed: true });
|
||||
toast.success(`Seeding ${torrent.files.length} file(s)`);
|
||||
beginProcessing();
|
||||
let torrent;
|
||||
try {
|
||||
torrent = client.seed(files, TORRENT_OPTIONS, (seeded) => {
|
||||
handleTorrentReady(seeded, { isSeed: true });
|
||||
toast.success(`Seeding ${seeded.files.length} file(s)`);
|
||||
});
|
||||
} catch (error) {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
userLogger.error("[beacon] Failed to seed files", error);
|
||||
toast?.error(error?.message || "Couldn't seed those files.");
|
||||
return;
|
||||
}
|
||||
watchTorrentForFailure(torrent);
|
||||
}
|
||||
|
||||
// A torrent can emit its own 'error' (bad/duplicate magnet) without the
|
||||
// client-level handler firing; surface it and drop the overlay so the add
|
||||
// fails visibly instead of spinning.
|
||||
function watchTorrentForFailure(torrent) {
|
||||
if (!torrent || typeof torrent.once !== "function") {
|
||||
return;
|
||||
}
|
||||
torrent.once("error", (error) => {
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
userLogger.error("[beacon] Torrent error", error);
|
||||
toast?.error(error?.message || "That torrent could not be loaded.");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -572,6 +640,7 @@ export function createBeaconApp({
|
||||
const errorHandler = (error) => {
|
||||
userLogger.error("[beacon] Torrent client error", error);
|
||||
toast?.error(error?.message || String(error));
|
||||
clearProcessingWatchdog();
|
||||
setProcessing(false);
|
||||
};
|
||||
client.on("error", errorHandler);
|
||||
@@ -610,6 +679,7 @@ export function createBeaconApp({
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
clearProcessingWatchdog();
|
||||
intervalHandles.forEach((id) => view.clearInterval(id));
|
||||
intervalHandles.length = 0;
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -1 +1 @@
|
||||
import{IS_DEV_MODE as ve,IS_VERBOSE_DEV_MODE as Ie,IS_LOCKDOWN_MODE as Oe,LOCKDOWN_TITLE as _t,LOCKDOWN_MESSAGE as St,ADMIN_SUPER_NPUB as wt,ADMIN_DM_IMAGE_URL as Lt,BITVID_WEBSITE_URL as At,BLOG_URL as Ct,COMMUNITY_URL as xt,NOSTR_URL as vt,GITHUB_URL as It,BETA_URL as Ot,DNS_URL as Bt,TIP_JAR_URL as Nt,MAX_WALLET_DEFAULT_ZAP as Mt,PLATFORM_FEE_PERCENT as Dt,PLATFORM_LUD16_OVERRIDE as Rt,DEFAULT_RELAY_URLS_OVERRIDE as Ut,ADMIN_WHITELIST_MODE_STORAGE_KEY as Ht,DEFAULT_WHITELIST_MODE_ENABLED as kt,ALLOW_NSFW_CONTENT as Ft,DEFAULT_TRUST_SEED_NPUBS as Vt,DEFAULT_BLUR_THRESHOLD as Pt,DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as Wt,DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as qt,DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as jt,WATCH_HISTORY_KIND as $t,WATCH_HISTORY_LIST_IDENTIFIER as Yt,WATCH_HISTORY_MAX_ITEMS as Kt,WATCH_HISTORY_BATCH_RESOLVE as zt,WATCH_HISTORY_BATCH_PAGE_SIZE as Gt,WATCH_HISTORY_PAYLOAD_MAX_BYTES as Xt,WATCH_HISTORY_FETCH_EVENT_LIMIT as Zt,WATCH_HISTORY_CACHE_TTL_MS as Jt,VIEW_COUNT_DEDUPE_WINDOW_SECONDS as Qt,VIEW_COUNT_BACKFILL_MAX_DAYS as er,VIEW_COUNT_CACHE_TTL_MS as tr,ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS as rr,DEFAULT_PLAYBACK_SOURCE as nr,DEFAULT_PLAYBACK_START_TIMEOUT as or}from"../config/instance-config.js";var re=typeof globalThis>"u"?null:globalThis,ne=e=>{if(typeof e=="boolean")return e;if(typeof e=="string"){let t=e.trim().toLowerCase();if(t==="true")return!0;if(t==="false")return!1}return null},ee=ne(re?.__BITVID_DEV_MODE_OVERRIDE__),te=ne(re?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__),U=ee===null?!!ve:ee;var Be=te===null?!!Ie:te;var Ne=!!Oe;typeof window<"u"&&(window.__BITVID_DEV_MODE__=U,window.__BITVID_VERBOSE_DEV_MODE__=Be,window.__BITVID_LOCKDOWN_MODE__=Ne);var k=()=>{};function Me(){return typeof console>"u"||console===null?null:console}function De(){let e=Me();if(!e)return{log:k,info:k,debug:k,warn:k,error:k};let t=typeof e.log=="function"?e.log.bind(e):k;return{log:typeof e.log=="function"?e.log.bind(e):t,info:typeof e.info=="function"?e.info.bind(e):t,debug:typeof e.debug=="function"?e.debug.bind(e):t,warn:typeof e.warn=="function"?e.warn.bind(e):t,error:typeof e.error=="function"?e.error.bind(e):t}}var _=De(),M="[bitvid]";function oe(e){if(e.length===0)return{args:e,force:!1};let t=e[e.length-1];if(t&&typeof t=="object"&&!Array.isArray(t)&&Object.prototype.hasOwnProperty.call(t,"force")){let r=[...e],o=r.pop();return{args:r,force:!!o.force}}return{args:e,force:!1}}var S=Object.freeze({log:(...e)=>{U&&_.log(...e)},info:(...e)=>{U&&_.info(...e)},debug:(...e)=>{U&&_.debug(...e)},warn:(...e)=>{U&&_.warn(...e)},error:(...e)=>{U&&_.error(...e)}}),b=Object.freeze({log:(...e)=>{let{args:t,force:r}=oe(e);if(!r&&t.length===0)return;if(r){_.log(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.log(`${M} ${o}`);return}o!==void 0&&_.log(M,o)},info:(...e)=>{let{args:t,force:r}=oe(e);if(!r&&t.length===0)return;if(r){_.info(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){_.info(`${M} ${o}`);return}o!==void 0&&_.info(M,o)},warn:(...e)=>{_.warn(M,...e)},error:(...e)=>{_.error(M,...e)}}),ae=Object.freeze({dev:S,user:b});function Re(e){return!e||typeof e!="object"?!1:Object.entries({dev:["log","info","debug","warn","error"],user:["log","info","warn","error"]}).every(([r,o])=>{let n=e[r];return!n||typeof n!="object"?!1:o.every(c=>typeof n[c]=="function")})}typeof window<"u"&&(Re(window.bitvidLogger)||(window.bitvidLogger=ae));Object.freeze(ae);import{THEME_ACCENT_OVERRIDES as se}from"../config/instance-config.js";var G="bitvid:theme",v="dark",Ue=new Set(["light","dark"]),ie={dark:"rgb(15, 23, 42)",light:"rgb(248, 250, 252)"},ce=["[data-theme-toggle]",'[data-action="toggle-theme"]',".js-theme-toggle","#themeToggle"],ue=Object.freeze({accent:"--color-accent",accentStrong:"--color-accent-strong",accentPressed:"--color-accent-pressed"}),le=new WeakSet,W=new Set,D=null,de=!1,F=null,I=()=>typeof window<"u"&&typeof document<"u",q=e=>{if(typeof e!="string")return null;let t=e.trim().toLowerCase();return t==="default"||t===""?v:Ue.has(t)?t:null},He=()=>{if(!I())return null;try{let e=window.localStorage.getItem(G);return q(e)}catch(e){return b.warn("Unable to read stored theme preference:",e),null}},ke=e=>{if(I())try{window.localStorage.setItem(G,e)}catch(t){b.warn("Unable to persist theme preference:",t)}},K=()=>{if(!I())return null;let{documentElement:e}=document;return e instanceof HTMLElement?e:null},Fe=e=>typeof e=="string"?e.trim():"",Ve=e=>{if(!e||typeof e!="object")return null;let t=Object.entries(e).reduce((r,[o,n])=>{if(!n||typeof n!="object")return r;let c=Object.entries(n).reduce((a,[u,d])=>(Object.prototype.hasOwnProperty.call(ue,u)&&(a[u]=d),a),{});return Object.keys(c).length>0&&(r[o]=c),r},{});return Object.keys(t).length>0?t:null},fe=e=>{F=Ve(e)};fe(typeof se<"u"?se:null);var pe=(e,t)=>{if(!e)return;let r=F&&typeof F=="object"&&F[t]&&typeof F[t]=="object"?F[t]:null,o=!1;Object.entries(ue).forEach(([n,c])=>{let a=r?r[n]:null,u=Fe(a);u?(e.style.setProperty(c,u),o=!0)Line truncated
|
||||
import{IS_DEV_MODE as Be,IS_VERBOSE_DEV_MODE as Me,IS_LOCKDOWN_MODE as De,LOCKDOWN_TITLE as Ct,LOCKDOWN_MESSAGE as xt,ADMIN_SUPER_NPUB as It,ADMIN_DM_IMAGE_URL as vt,BITVID_WEBSITE_URL as Ot,BLOG_URL as Nt,COMMUNITY_URL as Bt,NOSTR_URL as Mt,GITHUB_URL as Dt,BETA_URL as Rt,DNS_URL as Ut,TIP_JAR_URL as Ht,MAX_WALLET_DEFAULT_ZAP as kt,PLATFORM_FEE_PERCENT as Ft,PLATFORM_LUD16_OVERRIDE as Pt,DEFAULT_RELAY_URLS_OVERRIDE as Vt,ADMIN_WHITELIST_MODE_STORAGE_KEY as Wt,DEFAULT_WHITELIST_MODE_ENABLED as qt,ALLOW_NSFW_CONTENT as jt,FEATURE_NIP71_INGEST as $t,DEFAULT_TRUST_SEED_NPUBS as Yt,DEFAULT_BLUR_THRESHOLD as Gt,DEFAULT_AUTOPLAY_BLOCK_THRESHOLD as Kt,DEFAULT_TRUSTED_MUTE_HIDE_THRESHOLD as zt,DEFAULT_TRUSTED_SPAM_HIDE_THRESHOLD as Xt,WATCH_HISTORY_KIND as Zt,WATCH_HISTORY_LIST_IDENTIFIER as Jt,WATCH_HISTORY_MAX_ITEMS as Qt,WATCH_HISTORY_BATCH_RESOLVE as er,WATCH_HISTORY_BATCH_PAGE_SIZE as tr,WATCH_HISTORY_PAYLOAD_MAX_BYTES as rr,WATCH_HISTORY_FETCH_EVENT_LIMIT as nr,WATCH_HISTORY_CACHE_TTL_MS as or,VIEW_COUNT_DEDUPE_WINDOW_SECONDS as ar,VIEW_COUNT_BACKFILL_MAX_DAYS as sr,VIEW_COUNT_CACHE_TTL_MS as ir,ENSURE_PRESENCE_REBROADCAST_COOLDOWN_SECONDS as cr,DEFAULT_PLAYBACK_SOURCE as lr,DEFAULT_PLAYBACK_START_TIMEOUT as dr,ENABLE_NIP17_RELAY_WARNING as ur}from"../config/instance-config.js";var oe=typeof globalThis>"u"?null:globalThis,ae=e=>{if(typeof e=="boolean")return e;if(typeof e=="string"){let t=e.trim().toLowerCase();if(t==="true")return!0;if(t==="false")return!1}return null},re=ae(oe?.__BITVID_DEV_MODE_OVERRIDE__),ne=ae(oe?.__BITVID_VERBOSE_DEV_MODE_OVERRIDE__),U=re===null?!!Be:re;var Re=ne===null?!!Me:ne;var Ue=!!De;typeof window<"u"&&(window.__BITVID_DEV_MODE__=U,window.__BITVID_VERBOSE_DEV_MODE__=Re,window.__BITVID_LOCKDOWN_MODE__=Ue);var k=()=>{};function He(){return typeof console>"u"||console===null?null:console}function ke(){let e=He();if(!e)return{log:k,info:k,debug:k,warn:k,error:k};let t=typeof e.log=="function"?e.log.bind(e):k;return{log:typeof e.log=="function"?e.log.bind(e):t,info:typeof e.info=="function"?e.info.bind(e):t,debug:typeof e.debug=="function"?e.debug.bind(e):t,warn:typeof e.warn=="function"?e.warn.bind(e):t,error:typeof e.error=="function"?e.error.bind(e):t}}var w=ke(),M="[bitvid]";function se(e){if(e.length===0)return{args:e,force:!1};let t=e[e.length-1];if(t&&typeof t=="object"&&!Array.isArray(t)&&Object.prototype.hasOwnProperty.call(t,"force")){let r=[...e],o=r.pop();return{args:r,force:!!o.force}}return{args:e,force:!1}}var A=Object.freeze({log:(...e)=>{U&&w.log(...e)},info:(...e)=>{U&&w.info(...e)},debug:(...e)=>{U&&w.debug(...e)},warn:(...e)=>{U&&w.warn(...e)},error:(...e)=>{U&&w.error(...e)}}),m=Object.freeze({log:(...e)=>{let{args:t,force:r}=se(e);if(!r&&t.length===0)return;if(r){w.log(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){w.log(`${M} ${o}`);return}o!==void 0&&w.log(M,o)},info:(...e)=>{let{args:t,force:r}=se(e);if(!r&&t.length===0)return;if(r){w.info(M,...t);return}let[o,...n]=t;if(typeof o=="string"&&n.length===0){w.info(`${M} ${o}`);return}o!==void 0&&w.info(M,o)},warn:(...e)=>{w.warn(M,...e)},error:(...e)=>{w.error(M,...e)}}),ie=Object.freeze({dev:A,user:m});function Fe(e){return!e||typeof e!="object"?!1:Object.entries({dev:["log","info","debug","warn","error"],user:["log","info","warn","error"]}).every(([r,o])=>{let n=e[r];return!n||typeof n!="object"?!1:o.every(c=>typeof n[c]=="function")})}typeof window<"u"&&(Fe(window.bitvidLogger)||(window.bitvidLogger=ie));Object.freeze(ie);import{THEME_ACCENT_OVERRIDES as ce}from"../config/instance-config.js";var Z="bitvid:theme",O="dark",Pe=new Set(["light","dark"]),le={dark:"rgb(15, 23, 42)",light:"rgb(248, 250, 252)"},de=["[data-theme-toggle]",'[data-action="toggle-theme"]',".js-theme-toggle","#themeToggle"],pe=Object.freeze({accent:"--color-accent",accentStrong:"--color-accent-strong",accentPressed:"--color-accent-pressed"}),ue=new WeakSet,V=new Set,D=null,fe=!1,F=null,N=()=>typeof window<"u"&&typeof document<"u",W=e=>{if(typeof e!="string")return null;let t=e.trim().toLowerCase();return t==="default"||t===""?O:Pe.has(t)?t:null},Ve=()=>{if(!N())return null;try{let e=window.localStorage.getItem(Z);return W(e)}catch(e){return m.warn("Unable to read stored theme preference:",e),null}},We=e=>{if(N())try{window.localStorage.setItem(Z,e)}catch(t){m.warn("Unable to persist theme preference:",t)}},X=()=>{if(!N())return null;let{documentElement:e}=document;return e instanceof HTMLElement?e:null},qe=e=>typeof e=="string"?e.trim():"",je=e=>{if(!e||typeof e!="object")return null;let t=Object.entries(e).reduce((r,[o,n])=>{if(!n||typeof n!="object")return r;let c=Object.entries(n).reduce((a,[p,d])=>(Object.prototype.hasOwnProperty.call(pe,p)&&(a[p]=d),a),{});return Object.keys(c).length>0&&(r[o]=c),r},{});return Object.keys(t).length>0?t:null},me=e=>{F=je(e)};me(typeof ce<"u"?ce:null);var he=(e,t)=>{if(!e)return;let r=F&&typeof F=="object"&&F[t]&&typeof F[t]=="object"?F[t]:null,o=!1;Object.entries(pe).forEach(([n,c])=>Line truncated
|
||||
Reference in new issue
Block a user