browser-worker.mjs (10700B)
1 import {WASI,File,OpenFile,ConsoleStdout,PreopenDirectory} from './shim/index.js'; 2 import pins,{compilerHash} from './pins.mjs'; 3 import {moduleDependencies} from './site/module-stage.js'; 4 import {fetchBytes} from './site/bundle.js'; 5 import {environmentProfile} from './site/packages.js'; 6 import {frame,authenticatedBundle,loadFields,application,evaluation,snapshotFields,snapshotBundle,exportedTree,environmentFields,emptyEnvironment,dagBytes} from './bridge.mjs'; 7 let instance,booted=false,dead=false,stderr="",storedBundle=null,busy=false,compilerReady=false; 8 const dataFiles=new Map(); 9 const encoder=new TextEncoder(),decoder=new TextDecoder('utf-8',{fatal:true}); 10 function scalarText(s,limit){ 11 if(typeof s!=='string')throw Error('Expected string'); 12 let count=0; 13 for(const c of s){const n=c.codePointAt(0);if(++count>limit||n>=0xd800&&n<=0xdfff)throw Error('Invalid Unicode or scalar limit');} 14 return s; 15 } 16 async function checkedFile(name,corrupt){ 17 const response=await fetch(new URL('data/'+name,import.meta.url)); 18 if(!response.ok)throw Error('Data fetch failed'); 19 const bytes=new Uint8Array(await response.arrayBuffer()); 20 // Test-only fault injection can corrupt, never replace the trusted pin. 21 if(corrupt&&bytes.length)bytes[0]^=1; 22 const hash=Array.from(new Uint8Array(await crypto.subtle.digest('SHA-256',bytes)),b=>b.toString(16).padStart(2,'0')).join(''); 23 if(hash!==pins[name])throw Error('Prepared input SHA-256 mismatch'); 24 return new File(bytes,{readonly:true}); 25 } 26 function call(name,text){ 27 const e=instance.exports; 28 let ptr=0,status; 29 try{ 30 if(text===undefined)status=e[name](); 31 else{ 32 const bytes=typeof text==='string'?encoder.encode(text):text; 33 if(bytes.length>16777216)throw Error('Input byte limit'); 34 ptr=e.malloc(Math.max(bytes.length,1));if(!ptr)throw Error('Allocation failed'); 35 new Uint8Array(e.memory.buffer,ptr,bytes.length).set(bytes); 36 status=e[name](ptr,bytes.length); 37 } 38 }finally{if(ptr)e.free(ptr);} 39 const length=e.tricu_response_size(); 40 if(length<0||length>16777216)throw Error('Invalid response length'); 41 ptr=e.malloc(Math.max(length,1));if(!ptr)throw Error('Allocation failed'); 42 let output; 43 try{ 44 if(e.tricu_response_copy(ptr,length)!==length)throw Error('Response copy failed'); 45 // Refresh the view after every call: memory.grow detaches old views. 46 output=new Uint8Array(e.memory.buffer,ptr,length).slice(); 47 }finally{e.free(ptr);} 48 const decoded=decoder.decode(output); 49 if(status!==0){const error=Error(decoded.replace(/^error\n/,''));error.recoverable=status===1;throw error;} 50 if(!decoded.startsWith('ok\n'))throw Error('Invalid response tag'); 51 return output.subarray(3).slice().buffer; 52 } 53 onmessage=async({data:m})=>{ 54 if(busy){postMessage({id:m.id,ok:false,error:'Worker is busy',fatal:false});return;} 55 busy=true; 56 const begin=performance.now(); 57 try{ 58 if(dead)throw Error('Disposed runtime'); 59 let bytes,phases,scopeNames; 60 const encode=value=>encoder.encode(JSON.stringify(value)).buffer; 61 const operation=fields=>call('tricu_request',frame(fields)); 62 const safe=async action=>{try{return await action();}catch(e){e.recoverable=true;throw e;}}; 63 if(m.type==='init'){ 64 if(booted)throw Error('Already initialized');booted=true; 65 const files=m.compiler===false?[]:await Promise.all(Object.keys(pins).map(async name=>[name,await checkedFile(name,m.corrupt===name)])); 66 for(const [name,file] of files)dataFiles.set(name,file); 67 const fetched=performance.now(); 68 const response=await fetch(new URL('reactor.wasm',import.meta.url)); 69 if(!response.ok)throw Error('WASM fetch failed'); 70 const module=await WebAssembly.compileStreaming(response); 71 const compiled=performance.now(); 72 const wasi=new WASI([],[],[new OpenFile(new File([])),ConsoleStdout.lineBuffered(console.log),ConsoleStdout.lineBuffered(s=>{stderr=(stderr+s+"\n").slice(-4096);}),new PreopenDirectory('/data',dataFiles)]); 73 instance=await WebAssembly.instantiate(module,{wasi_snapshot_preview1:wasi.wasiImport}); 74 wasi.initialize(instance);instance.exports.tricu_rts_init(); 75 const initialized=performance.now(); 76 bytes=call(m.compiler===false?'tricu_initialize_empty':'tricu_initialize'); 77 compilerReady=m.compiler!==false; 78 phases={data_ms:fetched-begin,wasm_fetch_compile_ms:compiled-fetched,instantiate_rts_ms:initialized-compiled,load_ms:performance.now()-initialized}; 79 }else{ 80 if(!instance)throw Error('Not initialized'); 81 if(m.type==='compiler-init'){ 82 if(!compilerReady){ 83 const files=await safe(()=>Promise.all(Object.keys(pins).map(async name=>[name,await checkedFile(name)]))); 84 for(const [name,file] of files)dataFiles.set(name,file); 85 bytes=call('tricu_initialize');compilerReady=true; 86 }else bytes=encoder.encode('ready').buffer; 87 }else if(m.type==='expression'){ 88 await safe(()=>{scalarText(m.source,16384);if(typeof m.name!=='string'||!/^browserExpression[0-9]+$/.test(m.name)||!['pure','io'].includes(m.mode))throw Error('Invalid expression request');}); 89 bytes=operation(['expression',m.name,m.source,m.mode,m.output]); 90 }else if(m.type==='module-header'){ 91 const source=await safe(()=>scalarText(m.source,16384)); 92 bytes=operation(['module-header',source]); 93 }else if(m.type==='compile'){ 94 try { 95 const source=await safe(()=>{const text=scalarText(m.source,16384);if(!text.trim())throw Error('Enter a source unit or native module');return text;}); 96 const stage=JSON.parse(decoder.decode(operation(['module-stage',source]))); 97 const base=await safe(()=>{const url=new URL(m.base??'../../',import.meta.url);if(url.origin!==location.origin)throw Error('Cross-origin site base');return url;}); 98 if(stage.native){ 99 const dependencies=await safe(()=>moduleDependencies(base,stage,m.localModules)); 100 for(const [ref,bundle] of dependencies){ 101 const fields=await safe(()=>['module-dependency',ref,dagBytes(bundle.nodes),...bundle.exports.flatMap(e=>[e.name,String(e.root)])]); 102 operation(fields); 103 } 104 bytes=operation(['module-compile']); 105 }else{ 106 let environment=m.environment; 107 if(m.profileName!==undefined){ 108 environment=await safe(async()=>{ 109 const raw=JSON.parse(decoder.decode(await fetchBytes(new URL('environments.json',base),32768))); 110 const selected=environmentProfile(raw,m.profileName); 111 if(selected.compiler!==compilerHash)throw Error('Demo environment/compiler pin mismatch'); 112 return selected.profile?{bytes:new Uint8Array(await fetchBytes(new URL(`/_arboricx/bundle/hash/${selected.profile.hash}`,base))).buffer,hash:selected.profile.hash,names:selected.profile.names}:null; 113 }); 114 } 115 if(environment===undefined)bytes=call('tricu_compile',source); 116 else{ 117 let fields; 118 if(environment===null)fields=emptyEnvironment(); 119 else{ 120 const env=await safe(()=>authenticatedBundle(environment.bytes,environment.hash)); 121 fields=await safe(()=>environmentFields(env,environment.names)); 122 } 123 bytes=operation(['compile',source,...fields]); 124 } 125 } 126 scopeNames=JSON.parse(decoder.decode(operation(['module-scope']))); 127 storedBundle=null; 128 }catch(error){ 129 if(error.recoverable)operation(['module-abort']); 130 throw error; 131 } 132 }else if(m.type==='load'){ 133 const bundle=await safe(()=>authenticatedBundle(m.bytes,m.hash)); 134 const fields=await safe(()=>loadFields(bundle)); 135 bytes=operation(fields);storedBundle=bundle; 136 }else if(m.type==='evaluate'){ 137 const fields=await safe(()=>evaluation(m.name,m.arguments,m.output)); 138 bytes=operation(fields); 139 }else if(m.type==='module-snapshot'){ 140 bytes=operation(['module-snapshot']); 141 }else if(m.type==='preview'||m.type==='snapshot'){ 142 const fields=await safe(()=>snapshotFields(m.name,m.arguments,m.limit??(m.type==='preview'?64:100000),m.type==='snapshot')); 143 bytes=operation(fields); 144 }else if(m.type==='export'){ 145 if(storedBundle)bytes=encode(await safe(()=>exportedTree(storedBundle,m.name,m.format))); 146 else{ 147 const fields=await safe(()=>snapshotFields(m.name,[],m.limit??100000,true)); 148 const snapshot=JSON.parse(decoder.decode(operation(fields))); 149 bytes=encode(await safe(()=>exportedTree(snapshotBundle(snapshot,m.name),m.name,m.format))); 150 } 151 }else if(m.type==='io-start'){ 152 const args=await safe(()=>application(m.name,m.arguments)); 153 bytes=operation(['io-start',...args]); 154 }else if(m.type==='io-resume'){ 155 await safe(()=>{if(!Number.isSafeInteger(m.token)||m.token<1)throw Error('Invalid IO token');scalarText(m.value??'',256);}); 156 bytes=operation(['io-resume',String(m.token),m.value??'']); 157 }else if(m.type==='stats'){ 158 bytes=encode({linearMemoryBytes:instance.exports.memory.buffer.byteLength}); 159 }else if(m.type==='io-cancel'){ 160 bytes=operation(['io-cancel']); 161 }else if(m.type==='io-snapshot'){ 162 bytes=operation(['io-snapshot',String(m.limit??100000)]); 163 }else if(m.type==='io-view'){ 164 bytes=operation(['io-view',String(m.output)]); 165 }else if(m.type==='observe'){ 166 // The private wire uses newline-separated names/ternary arguments. 167 if(typeof m.name!=='string'||!m.name||m.name.length>128||/[\r\n]/.test(m.name)||!Array.isArray(m.arguments)||m.arguments.length>16||m.arguments.some(x=>typeof x!=='string'||x.length>50000||!/^[012]+$/.test(x))){const e=Error('Invalid observation request');e.recoverable=true;throw e;} 168 bytes=call('tricu_observe',[m.name,...m.arguments].join('\n')); 169 }else if(['dump','reset','collect'].includes(m.type)){bytes=call('tricu_'+m.type);if(m.type==='reset')storedBundle=null;} 170 else{const e=Error('Unknown operation');e.recoverable=true;throw e;} 171 } 172 postMessage({id:m.id,ok:true,bytes,milliseconds:performance.now()-begin,linearBytes:instance.exports.memory.buffer.byteLength,phases,scopeNames,compilerHash:['init','compiler-init'].includes(m.type)?(compilerReady?compilerHash:null):undefined},[bytes]); 173 }catch(error){ 174 const fatal=m.type==='init'||!error.recoverable; 175 if(fatal)dead=true; 176 postMessage({id:m.id,ok:false,error:error.message+(stderr?"\n"+stderr:""),fatal}); 177 if(fatal)close(); 178 }finally{busy=false;} 179 };