tricu

An interpreted language for exploring Tree Calculus
Log | Files | Refs | README | LICENSE

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 }