backend-server.mjs (10447B)
1 // Loopback-only test origin with bounded, token-authenticated fault/report routes. 2 import {createServer} from 'node:http'; 3 import {readFile} from 'node:fs/promises'; 4 import {resolve,extname,sep} from 'node:path'; 5 import {randomBytes} from 'node:crypto'; 6 import {fixtures} from './zig-negative-fixtures.mjs'; 7 export async function startSiteServer(directory,autoRun=false) { 8 const root=resolve(process.env.ARBORICX_SITE_ROOT||'ext/demo-site'),runtime=resolve(directory),registry=resolve('ext/demo-site/bundles'); 9 const assetManifest=await readFile(resolve(root,'assets.json'),'utf8').then(JSON.parse).catch(()=>null); 10 const assetRoot=assetManifest?resolve(root,assetManifest.base):root; 11 const control='/test-'+randomBytes(24).toString('hex'),requests=[],marks=[],transfers=[]; 12 let failRuntime=false,compilerMismatch=false,moduleMissing=null,moduleCorrupt=null,moduleManifest=null,deliver,fail; 13 const completion=new Promise((resolve,reject)=>{deliver=resolve;fail=reject;});completion.catch(()=>{}); 14 const server=createServer(async(req,res)=>{ 15 try { 16 const url=new URL(req.url,'http://localhost'),path=url.pathname;requests.push(process.env.TEST_ASSET_CACHE?path+url.search:path); 17 res.setHeader('Cache-Control','no-store'); 18 if(path===control+'/transfers'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify(transfers));return;} 19 if(path==='/nested/site/'+(assetManifest?.base??'')+'dist/probe-api.js'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(assetManifest?resolve(root,'nix-support/probe-api.js'):new URL('./probe-api.mjs',import.meta.url)));return;} 20 if(path==='/cold-load-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./cold-load-probe.mjs',import.meta.url)));return;} 21 if(path===control+'/requests'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify(requests));return;} 22 if(path===control+'/fault'||path===control+'/report'||path===control+'/phase') { 23 if(req.method!=='POST')throw Error('method'); 24 let text='';for await(const chunk of req){text+=chunk;if(text.length>262144)throw Error('control size');} 25 const data=JSON.parse(text); 26 if(path.endsWith('/phase'))marks.push({name:data.name,at:performance.now()}); 27 else if(path.endsWith('/fault')){failRuntime=data.runtime??failRuntime;compilerMismatch=data.compilerPin??compilerMismatch; 28 if('moduleMissing' in data)moduleMissing=data.moduleMissing; 29 if('moduleCorrupt' in data)moduleCorrupt=data.moduleCorrupt; 30 if('moduleManifest' in data)moduleManifest=data.moduleManifest;} 31 else {data.ok?deliver(data.report):fail(Error(data.error));} 32 res.end('ok');return; 33 } 34 if(path==='/asset-cache-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./asset-cache-probe.mjs',import.meta.url)));return;} 35 if(path==='/zig-negative.json'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify(fixtures()));return;} 36 if(path==='/zig-fixtures.json'){res.setHeader('Content-Type','application/json');const data=JSON.parse(await readFile(new URL('../demos.json',import.meta.url)));for(const id of ['parser','lexer'])data.demos[id]=JSON.parse(await readFile(new URL('../'+id+'-demo.json',import.meta.url)));res.end(JSON.stringify(data));return;} 37 if(path==='/zig-session-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./zig-session-probe.mjs',import.meta.url)));return;} 38 if(path==='/backend-resume-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-resume-probe.mjs',import.meta.url)));return;} 39 if(path==='/backend-benchmark.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-benchmark.mjs',import.meta.url)));return;} 40 if(path==='/evaluation-layout-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./evaluation-layout-probe.mjs',import.meta.url)));return;} 41 if(path==='/syntax-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./syntax-probe.mjs',import.meta.url)));return;} 42 if(path==='/local-module-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./local-module-probe.mjs',import.meta.url)));return;} 43 if(path==='/module-goldens.json'){res.setHeader('Content-Type','application/json');res.end(await readFile(new URL('./module-goldens.json',import.meta.url)));return;} 44 if(path==='/basic-examples-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./basic-examples-probe.mjs',import.meta.url)));return;} 45 if(path==='/import-picker-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./import-picker-probe.mjs',import.meta.url)));return;} 46 if(path==='/module-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./module-probe.mjs',import.meta.url)));return;} 47 if(path==='/ide-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./ide-probe.mjs',import.meta.url)));return;} 48 if(path==='/live-ux-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./live-ux-probe.mjs',import.meta.url)));return;} 49 if(path==='/backend-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-probe.mjs',import.meta.url)));return;} 50 if(failRuntime&&(path.endsWith('/reactor.wasm')||path.endsWith('/arboricx.wasm')))throw Error('Injected artifact failure'); 51 if(compilerMismatch&&path.endsWith('/compiler.json')) { 52 const descriptor=JSON.parse(await readFile(resolve(assetRoot,'compiler.json'))); 53 res.setHeader('Content-Type','application/json');res.end(JSON.stringify({...descriptor,hash:'0'.repeat(64)}));return; 54 } 55 // A second publication of the same immutable module exercises the history 56 // selector without changing the shipped release index or its identities. 57 if(process.env.TEST_IMPORT_PICKER&&path.endsWith('/reference/libraries.json')){ 58 const index=JSON.parse(await readFile(resolve(assetRoot,'reference/libraries.json'))); 59 const row=index.releases.find(r=>r.name==='tricu-lib-native-math'&&r.version==='0.1.0'); 60 index.releases.push({...row,version:'0.0.9'}); 61 res.setHeader('Content-Type','application/json');res.end(JSON.stringify(index));return; 62 } 63 if(moduleManifest&&path.endsWith('/packages.json')){ 64 const index=JSON.parse(await readFile(resolve(assetRoot,'packages.json'))); 65 const other=index.releases.find(r=>r.name==='tricu-lib-packages-io'); 66 index.releases=index.releases.map(r=>r.manifest===moduleManifest?{...r,hash:other.hash}:r); 67 res.setHeader('Content-Type','application/json');res.end(JSON.stringify(index));return; 68 } 69 let file; 70 if(path.startsWith('/_arboricx/bundle/hash/')) { 71 const hash=path.split('/').at(-1);if(!/^[a-f0-9]{64}$/.test(hash))throw Error('hash');if(hash===moduleMissing)throw Error('Missing module');file=resolve(registry,hash+'.arboricx'); 72 } else { 73 const prefix='/nested/site/';if(!path.startsWith(prefix))throw Error('mount'); 74 const relative=path.slice(prefix.length),base=relative.startsWith('runtime/haskell/')?runtime:root; 75 file=resolve(base,relative.startsWith('runtime/haskell/')?relative.slice('runtime/haskell/'.length):relative); 76 if(!file.startsWith(base+sep))throw Error('path'); 77 } 78 res.setHeader('Content-Type',({'.txt':'text/plain; charset=utf-8','.md':'text/plain; charset=utf-8','.tri':'text/plain; charset=utf-8','.html':'text/html','.js':'text/javascript','.mjs':'text/javascript','.wasm':'application/wasm','.json':'application/json','.css':'text/css','.svg':'image/svg+xml','.png':'image/png'})[extname(file)]??'application/octet-stream'); 79 let bytes=await readFile(file); 80 if(moduleCorrupt&&path==='/_arboricx/bundle/hash/'+moduleCorrupt){bytes=Buffer.from(bytes);bytes[0]^=1;} 81 if(process.env.TEST_COLD_LOAD&&path==='/nested/site/index.html')bytes=Buffer.from(bytes.toString().replace('<head>',`<head><script>new MutationObserver((_,observer)=>{if(document.getElementById('status')?.textContent==='Ready'){globalThis.__readyMs=performance.now();observer.disconnect();}}).observe(document.documentElement,{childList:true,subtree:true,characterData:true});</script>`)); 82 if(autoRun&&path==='/nested/site/index.html'&&!new URL(req.url,'http://localhost').searchParams.has('noProbe'))bytes=Buffer.from(bytes.toString()+`<script type="module"> 83 try{const m=await import(${JSON.stringify(process.env.TEST_BASIC_EXAMPLES?'/basic-examples-probe.mjs':process.env.TEST_IMPORT_PICKER?'/import-picker-probe.mjs':process.env.TEST_COLD_LOAD?'/cold-load-probe.mjs':process.env.TEST_ASSET_CACHE?'/asset-cache-probe.mjs':process.env.TEST_LOCAL_MODULES?'/local-module-probe.mjs':process.env.TEST_MODULES?'/module-probe.mjs':process.env.TEST_IDE?'/ide-probe.mjs':process.env.TEST_LIVE_UX?'/live-ux-probe.mjs':process.env.TEST_BENCHMARK?'/backend-benchmark.mjs':'/backend-probe.mjs')});const report=await m.run(${JSON.stringify(control)},${JSON.stringify(process.env.TEST_BACKEND||'haskell-wasm')},${!!process.env.BENCHMARK_PSS});await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:true,report})});} 84 catch(e){await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:false,error:String(e)+'\\n'+(e.stack||'')})});} 85 </script>`); 86 if(process.env.TEST_ASSET_CACHE && ['/nested/site/index.html','/nested/site/assets.json'].includes(path))res.setHeader('Cache-Control','no-cache'); 87 if(process.env.TEST_ASSET_CACHE && assetManifest && path.startsWith('/nested/site/'+assetManifest.base))res.setHeader('Cache-Control','public, max-age=31536000, immutable'); 88 transfers.push({path:path+url.search,bytes:bytes.length}); 89 res.end(bytes); 90 }catch{res.writeHead(404);res.end();} 91 }); 92 await new Promise(r=>server.listen(0,'127.0.0.1',r)); 93 return {server,url:`http://127.0.0.1:${server.address().port}/nested/site/index.html`,control,completion,marks}; 94 }