backend-server.mjs (12600B)
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,createHash} from 'node:crypto'; 6 import {fixtures} from './runtime-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==='/terminal-host.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./terminal-host.mjs',import.meta.url)));return;} 35 if(path==='/source-probe-helpers.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./source-probe-helpers.mjs',import.meta.url)));return;} 36 if(path==='/import-folds-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./import-folds-probe.mjs',import.meta.url)));return;} 37 if(path==='/definition-names-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./definition-names-probe.mjs',import.meta.url)));return;} 38 if(path==='/inline-io-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./inline-io-probe.mjs',import.meta.url)));return;} 39 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;} 40 if(path==='/runtime-fixtures.json'){res.setHeader('Content-Type','application/json');res.end(JSON.stringify(fixtures()));return;} 41 if(path==='/program-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;} 42 if(path==='/io-lifecycle-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-lifecycle-probe.mjs',import.meta.url)));return;} 43 if(path==='/io-source.json'&&process.env.IO_SOURCE_BUNDLE){const bytes=await readFile(process.env.IO_SOURCE_BUNDLE);res.setHeader('Content-Type','application/json');res.end(JSON.stringify({bytes:[...bytes],hash:createHash('sha256').update(bytes).digest('hex')}));return;} 44 if(path==='/io-public-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-public-probe.mjs',import.meta.url)));return;} 45 if(path==='/io-source-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-source-probe.mjs',import.meta.url)));return;} 46 if(path==='/io-session-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-session-probe.mjs',import.meta.url)));return;} 47 if(path==='/io-core-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-core-probe.mjs',import.meta.url)));return;} 48 if(path==='/io-protocol-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-protocol-probe.mjs',import.meta.url)));return;} 49 if(path==='/io-version-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./io-version-probe.mjs',import.meta.url)));return;} 50 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;} 51 if(path==='/backend-benchmark.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-benchmark.mjs',import.meta.url)));return;} 52 if(path==='/syntax-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./syntax-probe.mjs',import.meta.url)));return;} 53 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;} 54 if(path==='/module-goldens.json'){res.setHeader('Content-Type','application/json');res.end(await readFile(new URL('./module-goldens.json',import.meta.url)));return;} 55 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;} 56 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;} 57 if(path==='/module-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./module-probe.mjs',import.meta.url)));return;} 58 if(path==='/ide-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./ide-probe.mjs',import.meta.url)));return;} 59 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;} 60 if(path==='/backend-probe.mjs'){res.setHeader('Content-Type','text/javascript');res.end(await readFile(new URL('./backend-probe.mjs',import.meta.url)));return;} 61 if(failRuntime&&path.endsWith('/reactor.wasm'))throw Error('Injected artifact failure'); 62 if(compilerMismatch&&path.endsWith('/compiler.json')) { 63 const descriptor=JSON.parse(await readFile(resolve(assetRoot,'compiler.json'))); 64 res.setHeader('Content-Type','application/json');res.end(JSON.stringify({...descriptor,hash:'0'.repeat(64)}));return; 65 } 66 // A second publication of the same immutable module exercises the history 67 // selector without changing the shipped release index or its identities. 68 if(process.env.TEST_IMPORT_PICKER&&path.endsWith('/reference/libraries.json')){ 69 const index=JSON.parse(await readFile(resolve(assetRoot,'reference/libraries.json'))); 70 const row=index.releases.find(r=>r.name==='tricu-lib-native-math'&&r.version==='0.1.0'); 71 index.releases.push({...row,version:'0.0.9'}); 72 res.setHeader('Content-Type','application/json');res.end(JSON.stringify(index));return; 73 } 74 if(moduleManifest&&path.endsWith('/packages.json')){ 75 const index=JSON.parse(await readFile(resolve(assetRoot,'packages.json'))); 76 const other=index.releases.find(r=>r.name==='tricu-lib-packages-io'); 77 index.releases=index.releases.map(r=>r.manifest===moduleManifest?{...r,hash:other.hash}:r); 78 res.setHeader('Content-Type','application/json');res.end(JSON.stringify(index));return; 79 } 80 let file; 81 if(path.startsWith('/_arboricx/bundle/hash/')) { 82 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'); 83 } else { 84 const prefix='/nested/site/';if(!path.startsWith(prefix))throw Error('mount'); 85 const relative=path.slice(prefix.length),base=relative.startsWith('runtime/haskell/')?runtime:root; 86 file=resolve(base,relative.startsWith('runtime/haskell/')?relative.slice('runtime/haskell/'.length):relative); 87 if(!file.startsWith(base+sep))throw Error('path'); 88 } 89 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'); 90 let bytes=await readFile(file); 91 if(moduleCorrupt&&path==='/_arboricx/bundle/hash/'+moduleCorrupt){bytes=Buffer.from(bytes);bytes[0]^=1;} 92 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>`)); 93 if(autoRun&&path==='/nested/site/index.html'&&!new URL(req.url,'http://localhost').searchParams.has('noProbe'))bytes=Buffer.from(bytes.toString()+`<script type="module"> 94 try{const m=await import(${JSON.stringify(process.env.TEST_IMPORT_FOLDS?'/import-folds-probe.mjs':process.env.TEST_DEFINITION_NAMES?'/definition-names-probe.mjs':process.env.TEST_INLINE_IO?'/inline-io-probe.mjs':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})});} 95 catch(e){await fetch(${JSON.stringify(control+'/report')},{method:'POST',body:JSON.stringify({ok:false,error:String(e)+'\\n'+(e.stack||'')})});} 96 </script>`); 97 if(process.env.TEST_ASSET_CACHE && ['/nested/site/index.html','/nested/site/assets.json'].includes(path))res.setHeader('Cache-Control','no-cache'); 98 if(process.env.TEST_ASSET_CACHE && assetManifest && path.startsWith('/nested/site/'+assetManifest.base))res.setHeader('Cache-Control','public, max-age=31536000, immutable'); 99 transfers.push({path:path+url.search,bytes:bytes.length}); 100 res.end(bytes); 101 }catch{res.writeHead(404);res.end();} 102 }); 103 await new Promise(r=>server.listen(0,'127.0.0.1',r)); 104 return {server,url:`http://127.0.0.1:${server.address().port}/nested/site/index.html`,control,completion,marks}; 105 }