tricu

A lazy functional language based on Tree Calculus
Log | Files | Refs | README | LICENSE

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 }