From 3f4aaf539cbaae044a7374ea75be261dff5ceaa8 Mon Sep 17 00:00:00 2001 From: SAPHID Date: Sun, 25 Oct 2026 23:09:14 +0000 Subject: [PATCH] Clear stale Framey store listings during refresh --- tests/page/framey_store.mjs | 23 +++++++++++++++++++++++ ui/headset.html | 13 +++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/tests/page/framey_store.mjs b/tests/page/framey_store.mjs index 79ce521..50aa4f2 100644 --- a/tests/page/framey_store.mjs +++ b/tests/page/framey_store.mjs @@ -60,6 +60,7 @@ assert.equal(el('deckyJobStatus').textContent,'Installed Plugin 2'); assert.equal(el('deckyJobProgress').value,100); assert.equal(el('deckyJobCheck').hidden,true); assert.equal(session.has('frameyPluginJob'),false); + // A stopped Decky service must still offer recovery by removing its owned plugin. ctx.mockApi=async path => path==='/api/decky' ? {managed:true,running:false,plugins:[{storeId:2,name:'Plugin 2',version:'1.0'}]} : @@ -81,6 +82,28 @@ await vm.runInContext('deckyChange("update",{id:2,name:"Plugin 2",sha256:"b".rep assert.match(el('deckyJobStatus').textContent,/Error: archive digest mismatch/); assert.equal(session.has('frameyPluginJob'),false); +// A failed refresh must remove old install buttons. A late older response +// must not replace the newer catalog the wearer just requested. +ctx.mockApi=async path => {assert.equal(path,'/api/decky/store');throw Error('offline');}; +vm.runInContext('api=mockApi',ctx); +await vm.runInContext('loadDeckyStore()',ctx); +assert.match(el('deckyStoreSummary').textContent,/Store unavailable: offline/); +assert.equal(el('deckyStoreList').children.length,0); +let finishOld, finishNew; +ctx.mockApi=path => {assert.equal(path,'/api/decky/store'); + return new Promise(resolve=>{if(!finishOld)finishOld=resolve;else finishNew=resolve;});}; +vm.runInContext('api=mockApi',ctx); +const old=vm.runInContext('loadDeckyStore()',ctx); +const current=vm.runInContext('loadDeckyStore()',ctx); +assert.equal(el('deckyStoreList').children.length,0); +assert.equal(el('deckyStoreSummary').textContent,'Loading official Decky store…'); +finishNew({plugins:[{id:98,name:'Current',version:'2.0',description:'Latest'}]}); +await current; +finishOld({plugins:[{id:97,name:'Stale',version:'1.0',description:'Older'}]}); +await old; +assert.match(el('deckyStoreList').children[0].children[0].textContent,/Current/); +assert.doesNotMatch(el('deckyStoreList').children[0].children[0].textContent,/Stale/); + // A saved job does not survive a server restart. Clear it instead of offering // Check result forever for an ID that the new server cannot know. session.set('frameyPluginJob',JSON.stringify({job:'old-job',action:'install',name:'Plugin 2'})); diff --git a/ui/headset.html b/ui/headset.html index 8c6f0ca..911e501 100644 --- a/ui/headset.html +++ b/ui/headset.html @@ -103,7 +103,8 @@ async function refreshGames(){const list=byId('games');try{const data=await api( catch(error){byId('actionStatus').textContent=error.message;quit.disabled=false;}}; row.append(name,quit);list.append(row);}if(!data.running.length)list.textContent='No games are running.'; }catch(error){list.textContent=error.message;}} -let deckyState=null, deckyStore=[], deckyStoreIndex=0, activeFrameyId=null, frameySession=null, deckyBusy=false; +let deckyState=null, deckyStore=[], deckyStoreIndex=0, deckyStoreRequest=0, + activeFrameyId=null, frameySession=null, deckyBusy=false; async function refreshDecky(){try{deckyState=await api('/api/decky'); byId('deckyState').textContent=deckyState.running?'Decky loader is running. Backend loading is not a VR menu check.': deckyState.managed?'Decky is stopped. You can remove a managed plugin to recover, then start Decky from Frame Control on your computer.': @@ -117,10 +118,14 @@ async function refreshDecky(){try{deckyState=await api('/api/decky'); button.disabled=deckyBusy;button.onclick=()=>deckyChange('remove',{id:plugin.storeId,name:plugin.name});row.append(button);} list.append(row);}if(!deckyState.plugins?.length)list.textContent='No managed plugins installed.'; showDeckyStore();}catch(error){byId('deckyState').textContent=error.message;}} -async function loadDeckyStore(){byId('deckyStorePanel').hidden=false; +async function loadDeckyStore(){const request=++deckyStoreRequest;byId('deckyStorePanel').hidden=false; + deckyStore=[];deckyStoreIndex=0;showDeckyStore(); byId('deckyStoreSummary').textContent='Loading official Decky store…'; - try{const data=await api('/api/decky/store');deckyStore=data.plugins;deckyStoreIndex=0;showDeckyStore();} - catch(error){byId('deckyStoreSummary').textContent=`Store unavailable: ${error.message}`;}} + try{const data=await api('/api/decky/store');if(request!==deckyStoreRequest)return; + deckyStore=data.plugins;deckyStoreIndex=0;showDeckyStore();} + catch(error){if(request!==deckyStoreRequest)return; + deckyStore=[];deckyStoreIndex=0;showDeckyStore(); + byId('deckyStoreSummary').textContent=`Store unavailable: ${error.message}`;}} function showDeckyStore(){const list=byId('deckyStoreList');list.replaceChildren(); const q=byId('deckySearch').value.trim().toLowerCase(); const filter=byId('deckyStoreFilter').value;