Add authenticated protocol 2 and TypeScript WASM client

This commit is contained in:
sergeych committed 2026-10-19 04:17:34 +04:00
1 parent 88ceeb2ae9
commit 237d404bcd
52 files changed
+3249 -150

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
// Starts only the disposable localhost fixture; never connects to deployment hosts.
import { spawn } from 'node:child_process';
import net from 'node:net';
import { setTimeout as pause } from 'node:timers/promises';
import { existsSync } from 'node:fs';
const executable = new URL('../evaluation/server/build/install/server/bin/server', import.meta.url).pathname;
if (!existsSync(executable)) throw Error('Build the fixture first: npm run fixture:build');
const listener = net.createServer();
await new Promise((resolve, reject) => listener.once('error', reject).listen(0, '127.0.0.1', resolve));
const port = listener.address().port;
await new Promise(resolve => listener.close(resolve));
const server = spawn(executable, [String(port)], { stdio: ['ignore', 'pipe', 'pipe'] });
let diagnostics = ''; let exited = false;
server.stdout.on('data', b => { diagnostics = (diagnostics + b).slice(-8000); });
server.stderr.on('data', b => { diagnostics = (diagnostics + b).slice(-8000); });
server.on('exit', () => { exited = true; });
server.on('error', e => { diagnostics += e.message; exited = true; });
try {
let ready = false;
for (let n = 0; n < 100 && !exited; n++) {
ready = await new Promise(resolve => {
const socket = net.connect(port, '127.0.0.1');
socket.once('connect', () => { socket.destroy(); resolve(true); });
socket.once('error', () => resolve(false));
});
if (ready) break;
await pause(100);
}
if (!ready) throw Error(`Local JVM fixture did not start:\n${diagnostics}`);
const run = spawn(process.execPath, ['--test', 'test/interop.test.mjs'], { stdio: 'inherit', env: { ...process.env, KILOPARSEC_TEST_URL: `ws://127.0.0.1:${port}/kp` } });
process.exitCode = await new Promise(resolve => run.once('exit', code => resolve(code ?? 1)));
} finally {
server.kill('SIGTERM');
if (!exited) {
const hardStop = setTimeout(() => server.kill('SIGKILL'), 3000);
await new Promise(resolve => { if (exited) resolve(); else server.once('exit', resolve); });
clearTimeout(hardStop);
}
}
+51
View File
@@ -0,0 +1,51 @@
/** Optimized Kotlin/Wasm baseline; includes all required JS and sodium WASM. */
import { build } from 'esbuild';
import { createHash } from 'node:crypto';
import { mkdir, copyFile, readFile, writeFile } from 'node:fs/promises';
import { gzipSync } from 'node:zlib';
import { spawnSync } from 'node:child_process';
import { fileURLToPath, pathToFileURL } from 'node:url';
import path from 'node:path';
const root = fileURLToPath(new URL('../', import.meta.url));
const output = path.join(root, 'evaluation/build/measurement/wasm');
await mkdir(output, { recursive: true });
await build({
absWorkingDir: root, entryPoints: ['evaluation/wasm-entry.mjs'],
outfile: path.join(output, 'client.mjs'), bundle: true, minify: true,
platform: 'node', format: 'esm', sourcemap: false,
banner: { js: "import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); const __filename = import.meta.filename; const __dirname = import.meta.dirname;" },
});
const wasmName = 'kiloparsec-evaluation-wasm-baseline.wasm';
await copyFile(path.join(root, 'evaluation/wasm-baseline/build/compileSync/wasmJs/main/productionExecutable/optimized', wasmName), path.join(output, wasmName));
const assets = [];
for (const name of ['client.mjs', wasmName]) {
const bytes = await readFile(path.join(output, name));
assets.push({ path: `evaluation/build/measurement/wasm/${name}`, sha256: createHash('sha256').update(bytes).digest('hex'), rawBytes: bytes.length, gzipBytes: gzipSync(bytes, { level: 9 }).length });
}
const samples = [];
for (let i = 0; i < 5; i++) {
const program = `const before=process.memoryUsage().rss; const start=performance.now();
try { const {api}=await import(${JSON.stringify(pathToFileURL(path.join(output, 'client.mjs')).href)});
await new Promise((resolve,reject)=>api.initializeIdentity(resolve,reject)); api.close();
console.log(JSON.stringify({milliseconds:performance.now()-start,rss:process.memoryUsage().rss,rssDelta:process.memoryUsage().rss-before}));
} catch(e) { console.error(String(e)); process.exitCode=1; }`;
const child = spawnSync(process.execPath, ['--input-type=module', '-e', program], { encoding: 'utf8', timeout: 15000 });
if (child.status !== 0) throw Error(child.stderr || String(child.error));
samples.push(JSON.parse(child.stdout.trim().split('\n').at(-1)));
}
const median = key => samples.map(s => s[key]).sort((a,b)=>a-b)[2];
const report = {
measuredAt: new Date().toISOString(), node: process.version, platform: process.platform,
architecture: process.arch, kotlin: '2.2.21', sodiumWrapper: '0.7.15', rawSodium: '0.7.16', sourceVersion: '0.8.0-SNAPSHOT', protocol: 2,
notes: [
'Actual optimized production Kotlin/Wasm executable, not klib or test runtime.',
'Minified Node ESM loader includes all sodium and joda dependencies; Kotlin wasm is a separate asset.',
'Sodium WASM is embedded in client.mjs. Gzip level9 is applied independently to each transferred asset and summed.',
'Full DH, expected server identity pin, client identity and echo RPC retained. Export bridge uses strings/callbacks.',
'Cold fresh Node process per sample, module loading plus sodium init plus one new Ed25519 identity; process startup excluded.',
], assets,
rawBytes: assets.reduce((n,a)=>n+a.rawBytes,0), gzipBytes: assets.reduce((n,a)=>n+a.gzipBytes,0),
medianMilliseconds: median('milliseconds'), medianRssBytes: median('rss'), medianRssDeltaBytes: median('rssDelta'), samples,
};
await writeFile(path.join(output, 'results.json'), JSON.stringify(report,null,2)+'\n');
console.log(JSON.stringify(report,null,2));
+36
View File
@@ -0,0 +1,36 @@
/** Same-platform bundled measurements; run `npm run build` and Kotlin production build first. */
import { build } from 'esbuild';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import { gzipSync } from 'node:zlib';
import { spawnSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
const root = fileURLToPath(new URL('../', import.meta.url));
const output = path.join(root, 'evaluation/build/measurement');
await mkdir(output, { recursive: true });
const entries = { ts: 'dist/index.js', kotlin: 'evaluation/baseline-entry.cjs' };
const report = { protocol: 2, sourceVersion: '0.8.0-SNAPSHOT', measuredAt: new Date().toISOString(), node: process.version, platform: process.platform, architecture: process.arch, sodiumWrapper: '0.7.15', notes: [
'Node CJS bundles, same esbuild settings: bundle/minify/platform=node; gzip level9, no source maps.',
'Both include sodium WASM. TS includes all exported API; Kotlin retains only baseline raw bridge, identity init and echo API.',
'Cold fresh Node subprocess per sample; elapsed begins immediately before module require, includes WASM init and one new Ed25519 identity.',
'RSS includes Node VM; delta measures process resident growth after workload. Five samples, use medians; no process startup time included.',
'These figures compare retained subsets, not full feature equivalence or proof of browser runtime compatibility.'
], artifacts: {} };
for (const [name, entry] of Object.entries(entries)) {
const outfile = path.join(output, `${name}.cjs`);
await build({ absWorkingDir: root, entryPoints: [entry], outfile, bundle: true, minify: true, platform: 'node', format: 'cjs', sourcemap: false, logLevel: 'warning' });
const data = await readFile(outfile);
const samples = [];
for (let i = 0; i < 5; i++) {
const program = `const before=process.memoryUsage().rss; const start=performance.now();
(async()=>{const m=require(${JSON.stringify(outfile)}); ${name === 'ts' ? 'await m.generateSigningKey();' : 'const api=await m.load(); await api.initializeIdentity();'}
console.log(JSON.stringify({milliseconds:performance.now()-start,rss:process.memoryUsage().rss,rssDelta:process.memoryUsage().rss-before}));})().catch(e=>{console.error(e);process.exitCode=1});`;
const child = spawnSync(process.execPath, ['-e', program], { encoding: 'utf8', timeout: 15000 });
if (child.status !== 0) throw Error(`${name}: ${child.stderr || child.error}`);
samples.push(JSON.parse(child.stdout.trim().split('\n').at(-1)));
}
const median = key => samples.map(s => s[key]).sort((a,b)=>a-b)[2];
report.artifacts[name] = { rawBytes: data.length, gzipBytes: gzipSync(data, { level: 9 }).length, medianMilliseconds: median('milliseconds'), medianRssBytes: median('rss'), medianRssDeltaBytes: median('rssDelta'), samples };
}
await writeFile(path.join(output, 'results.json'), JSON.stringify(report, null, 2) + '\n');
console.log(JSON.stringify(report, null, 2));