From 02fec484d6e2580d5c9af64b8bae8dd0f1881e81 Mon Sep 17 00:00:00 2001 From: SAPHID Date: Mon, 26 Oct 2026 22:46:50 +0000 Subject: [PATCH] Keep Framey store job recoverable without session storage --- docs/framey-loader.md | 4 ++++ tests/page/framey_store.mjs | 43 +++++++++++++++++++++++++++++++++++++ ui/headset.html | 38 ++++++++++++++++++++++---------- 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/docs/framey-loader.md b/docs/framey-loader.md index 5b42e84..addf0fb 100644 --- a/docs/framey-loader.md +++ b/docs/framey-loader.md @@ -204,6 +204,10 @@ browser session, so **Check result** can resume polling after a navigation or temporary connection error. After a polling error, Install, Update, and Remove stay disabled while the saved job's result is unknown; use **Check result** before starting another change. +If Chromium denies session storage, the open panel keeps the accepted job ID in +memory and **Check result** still works after a connection error. A page reload +loses that memory fallback; wait for the old job to finish and refresh installed +status before starting another change. Refresh the installed list before starting another operation after an error. The headset panel and desktop app send the hash of the version you saw; if the official store changes that version before the job diff --git a/tests/page/framey_store.mjs b/tests/page/framey_store.mjs index 8a24c3f..cab9537 100644 --- a/tests/page/framey_store.mjs +++ b/tests/page/framey_store.mjs @@ -244,3 +244,46 @@ vm.runInContext('api=mockApi',ctx); await vm.runInContext('refreshDecky()',ctx); assert.equal(el('frameyPanel').hidden,true); assert.match(el('deckyState').textContent,/panel was closed until status can be refreshed/); + +// A denied sessionStorage must not lose an accepted job in the current panel. +// Keep Update/Remove disabled after a polling outage and allow Check result. +for(const method of ['getItem','setItem','removeItem']) + ctx.sessionStorage[method]=()=>{throw Error('storage denied');}; +let accepted=0; +ctx.mockApi=async path => { + if(path==='/api/decky/plugin'){accepted++;return {job:'memory-job'};} + if(path.startsWith('/api/job'))throw Error('connection lost'); + if(path==='/api/decky')return {managed:true,running:true, + plugins:[{storeId:2,name:'Plugin 2',version:'1.0'}]}; + throw Error(`unexpected ${path}`); +}; +vm.runInContext('api=mockApi;deckyState={managed:true,running:true,plugins:[]};' + + 'deckyStore=[{id:2,name:"Plugin 2",version:"1.0",description:"A panel",root:false}];showDeckyStore()',ctx); +await vm.runInContext('deckyChange("install",{id:2,name:"Plugin 2",sha256:"a".repeat(64)})',ctx); +assert.equal(accepted,1); +assert.equal(vm.runInContext('deckyJobPending()',ctx),true); +assert.equal(el('deckyJobCheck').hidden,false); +assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,true); +await vm.runInContext('deckyChange("remove",{id:2,name:"Plugin 2"})',ctx); +assert.equal(accepted,1); +ctx.mockApi=async path => { + if(path.startsWith('/api/job'))return {done:true,message:'Installed Plugin 2'}; + if(path==='/api/decky')return {managed:true,running:true, + plugins:[{storeId:2,name:'Plugin 2',version:'1.0'}]}; + throw Error(`unexpected ${path}`); +}; +vm.runInContext('api=mockApi',ctx); +await vm.runInContext('checkDeckyJob()',ctx); +assert.equal(vm.runInContext('deckyJobPending()',ctx),false); +assert.equal(el('deckyJobCheck').hidden,true); +assert.equal(el('deckyInstalled').children[0].children.find(child=>child.textContent==='Remove').disabled,false); + +// Opening the headset panel with a key in its URL must survive denied storage. +const deniedDocument={getElementById:id=>new Element(id),createElement:()=>new Element()}; +const deniedBoot=vm.createContext({document:deniedDocument, + location:{hash:'#private-key',pathname:'/'},history:{replaceState(){}}, + sessionStorage:{getItem(){throw Error('storage denied');},setItem(){throw Error('storage denied');}}, + window:{addEventListener(){}},setInterval(){},setTimeout(){},confirm:()=>true,console, + fetch(){throw Error('No network expected');}}); +vm.runInContext(script,deniedBoot); +assert.equal(vm.runInContext('key',deniedBoot),'private-key'); diff --git a/ui/headset.html b/ui/headset.html index 856a9e8..e2a6d54 100644 --- a/ui/headset.html +++ b/ui/headset.html @@ -64,8 +64,8 @@ iframe{display:block;width:100%;height:560px;background:#0d1925;border:1px solid