File size: 4,512 Bytes
7d419bd
 
 
 
 
 
 
 
 
 
c77f6d9
7d419bd
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// A bounded, reference-counted queue. A shared scene has one job regardless of how many cards display it.
export class AssetQueue {
  constructor({load,dispose,maxConcurrent=2,maxIdle=1,maxBytes=128*1024*1024,onChange=()=>{}}){Object.assign(this,{load,dispose,maxConcurrent,maxIdle,maxBytes,onChange});this.entries=new Map();this.running=0;this.peakRunning=0;}
  retain(key,listener,priority=()=>0){
    let e=this.entries.get(key);
    if(!e||e.state==='error'||e.state==='cancelled'){
      e={key,users:new Set(),state:'queued',asset:null,controller:new AbortController(),last:Date.now(),bytes:0};this.entries.set(key,e);
    }
    const user={listener,priority};e.users.add(user);e.last=Date.now();queueMicrotask(()=>{if(e.users.has(user))listener(e);this.pump();});this.changed();
    let released=false;
    return {entry:e,release:()=>{if(released)return;released=true;e.users.delete(user);e.last=Date.now();if(!e.users.size&&e.asset)e.asset.materials?.pause();if(!e.users.size&&!e.asset){e.state='cancelled';e.controller.abort();if(this.entries.get(key)===e)this.entries.delete(key);}this.evict();this.pump();this.changed();}};
  }
  notify(e,state,detail={}){if(e.controller.signal.aborted)return;Object.assign(e,detail,{state});for(const u of e.users)u.listener(e);this.changed();}
  pump(){
    while(this.running<this.maxConcurrent){const queued=[...this.entries.values()].filter(e=>e.state==='queued'&&e.users.size).sort((a,b)=>Math.min(...[...a.users].map(u=>u.priority()))-Math.min(...[...b.users].map(u=>u.priority())));if(!queued.length)break;const e=queued[0];e.state='loading';this.running++;this.peakRunning=Math.max(this.peakRunning,this.running);this.run(e);}
  }
  async run(e){
    try{
      let asset;
      for(let attempt=0;attempt<2;attempt++){
        try{asset=await this.load(e.key,e.controller.signal,(state,detail)=>this.notify(e,state,detail));break;}
        catch(error){if(e.controller.signal.aborted)throw error;if(attempt===1)throw error;this.notify(e,'retrying');await new Promise(r=>setTimeout(r,800));}
      }
      if(e.controller.signal.aborted||!e.users.size){this.dispose(asset);return;}
      e.asset=asset;e.bytes=asset.bytes||0;this.notify(e,'ready');
    }catch(error){if(!e.controller.signal.aborted)this.notify(e,'error',{error});}
    finally{this.running--;this.evict();this.pump();this.changed();}
  }
  evict(){
    const idle=[...this.entries.values()].filter(e=>!e.users.size&&e.asset).sort((a,b)=>a.last-b.last);let total=[...this.entries.values()].reduce((n,e)=>n+e.bytes,0);
    while(idle.length&&(idle.length>this.maxIdle||total>this.maxBytes)){const e=idle.shift();total-=e.bytes;this.dispose(e.asset);e.asset=null;if(this.entries.get(e.key)===e)this.entries.delete(e.key);}
  }
  changed(){this.onChange({running:this.running,peak:this.peakRunning,queued:[...this.entries.values()].filter(e=>e.state==='queued').length,cached:[...this.entries.values()].filter(e=>e.asset).length,bytes:[...this.entries.values()].reduce((n,e)=>n+e.bytes,0)});}
}

// A stalled body must not leave a loading card forever. Idle and total limits cover headers and streamed bytes.
export async function fetchSceneBytes(url,signal,progress,{idleMs=20000,totalMs=90000,fetcher=fetch}={}){
  const abort=new AbortController();let idle,total,reason='';
  const stop=()=>abort.abort();signal.addEventListener('abort',stop,{once:true});if(signal.aborted)stop();
  const reset=()=>{clearTimeout(idle);idle=setTimeout(()=>{reason='The scene download stopped responding.';abort.abort();},idleMs);};
  reset();total=setTimeout(()=>{reason='The scene download took too long.';abort.abort();},totalMs);
  try{
    const response=await fetcher(url,{signal:abort.signal});if(!response.ok)throw new Error('Scene request failed (HTTP '+response.status+').');
    const size=Number(response.headers.get('content-length'))||0,reader=response.body.getReader(),chunks=[];let received=0,last=0;
    for(;;){const {done,value}=await reader.read();if(done)break;reset();chunks.push(value);received+=value.length;if(Date.now()-last>120){progress({received,total:size});last=Date.now();}}
    if(!received||size&&received!==size)throw new Error('The scene download was incomplete.');
    progress({received,total:size});const bytes=new Uint8Array(received);let at=0;for(const c of chunks){bytes.set(c,at);at+=c.length;}return bytes;
  }catch(error){if(reason)throw new Error(reason);throw error;}
  finally{clearTimeout(idle);clearTimeout(total);signal.removeEventListener('abort',stop);}
}