Poindexter/npm/poindexter-wasm/loader.js
google-labs-jules[bot] e878ea9db4 feat(wasm): Improve WASM error handling and loader
This commit introduces a comprehensive set of improvements to the error handling and loading mechanism of the WebAssembly (WASM) module.

The key changes include:

- **Structured Error Handling:** Replaced generic string-based errors with a structured `WasmError` type in the Go WASM wrapper. This provides standardized error codes (`bad_request`, `not_found`, `conflict`) and clear messages, allowing JavaScript clients to handle errors programmatically.

- **Isomorphic WASM Loader:** Refactored the JavaScript loader (`loader.js`) to be isomorphic, enabling it to run seamlessly in both browser and Node.js environments. The loader now detects the environment and uses the appropriate mechanism for loading the WASM binary and `wasm_exec.js`.

- **Type Conversion Fix:** Resolved a panic (`panic: ValueOf: invalid value`) that occurred when returning `[]float64` slices from Go to JavaScript. A new `pointToJS` helper function now correctly converts these slices to `[]any`, ensuring proper data marshalling.

- **Improved Smoke Test:** Enhanced the WASM smoke test (`smoke.mjs`) to verify the new structured error handling and to correctly handle the API's response format.

- **Configuration Updates:** Updated the `.golangci.yml` configuration to be compatible with the latest version of `golangci-lint`.

In addition to these changes, this commit also includes a new `AUDIT-ERROR-HANDLING.md` file, which documents the findings of a thorough audit of the project's error handling and logging practices.

Co-authored-by: Snider <631881+Snider@users.noreply.github.com>
2026-02-02 01:27:55 +00:00

161 lines
6.4 KiB
JavaScript

// ESM loader for Poindexter WASM
// Usage:
// import { init } from '@snider/poindexter-wasm';
// const px = await init();
// const tree = await px.newTree(2);
// await tree.insert({ id: 'a', coords: [0,0], value: 'A' });
// const res = await tree.nearest([0.1, 0.2]);
// --- Environment detection ---
const isBrowser = typeof window !== 'undefined';
const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null;
// --- Browser-specific helpers ---
async function loadScriptOnce(src) {
return new Promise((resolve, reject) => {
// If already present, resolve immediately
if (document.querySelector(`script[src="${src}"]`)) return resolve();
const s = document.createElement('script');
s.src = src;
s.onload = () => resolve();
s.onerror = (e) => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(s);
});
}
// --- Loader logic ---
async function ensureWasmExec(url) {
if (typeof globalThis.Go === 'function') return;
if (isBrowser) {
await loadScriptOnce(url);
} else if (isNode) {
const { fileURLToPath } = await import('url');
const wasmExecPath = fileURLToPath(url);
await import(wasmExecPath);
} else {
throw new Error(`Unsupported environment: cannot load ${url}`);
}
if (typeof globalThis.Go !== 'function') {
throw new Error('wasm_exec.js did not define globalThis.Go');
}
}
function unwrap(result) {
if (!result || typeof result !== 'object') {
throw new Error(`bad/unexpected result type from WASM: ${typeof result}`);
}
if (result.ok) {
return result.data;
}
// Handle structured errors, which may be nested
const errorPayload = result.error || result;
if (errorPayload && typeof errorPayload === 'object') {
const err = new Error(errorPayload.message || 'unknown WASM error');
err.code = errorPayload.code;
throw err;
}
// Fallback for simple string errors
throw new Error(errorPayload || 'unknown WASM error');
}
function call(name, ...args) {
const fn = globalThis[name];
if (typeof fn !== 'function') throw new Error(`WASM function ${name} not found`);
return unwrap(fn(...args));
}
class PxTree {
constructor(treeId) { this.treeId = treeId; }
// Core operations
async len() { return call('pxTreeLen', this.treeId); }
async dim() { return call('pxTreeDim', this.treeId); }
async insert(point) { return call('pxInsert', this.treeId, point); }
async deleteByID(id) { return call('pxDeleteByID', this.treeId, id); }
async nearest(query) { return call('pxNearest', this.treeId, query); }
async kNearest(query, k) { return call('pxKNearest', this.treeId, query, k); }
async radius(query, r) { return call('pxRadius', this.treeId, query, r); }
async exportJSON() { return call('pxExportJSON', this.treeId); }
// Analytics operations
async getAnalytics() { return call('pxGetAnalytics', this.treeId); }
async getPeerStats() { return call('pxGetPeerStats', this.treeId); }
async getTopPeers(n) { return call('pxGetTopPeers', this.treeId, n); }
async getAxisDistributions(axisNames) { return call('pxGetAxisDistributions', this.treeId, axisNames); }
async resetAnalytics() { return call('pxResetAnalytics', this.treeId); }
}
export async function init(options = {}) {
const {
wasmURL = new URL('./dist/poindexter.wasm', import.meta.url).toString(),
wasmExecURL = new URL('./dist/wasm_exec.js', import.meta.url).toString(),
instantiateWasm // optional custom instantiator: (source, importObject) => WebAssembly.Instance
} = options;
await ensureWasmExec(wasmExecURL);
const go = new globalThis.Go();
let result;
if (instantiateWasm) {
let source;
if (isBrowser) {
source = await fetch(wasmURL).then(r => r.arrayBuffer());
} else {
const fs = await import('fs/promises');
const { fileURLToPath } = await import('url');
source = await fs.readFile(fileURLToPath(wasmURL));
}
const inst = await instantiateWasm(source, go.importObject);
result = { instance: inst };
} else if (isBrowser && WebAssembly.instantiateStreaming) {
result = await WebAssembly.instantiateStreaming(fetch(wasmURL), go.importObject);
} else {
let bytes;
if (isBrowser) {
const resp = await fetch(wasmURL);
bytes = await resp.arrayBuffer();
} else {
const fs = await import('fs/promises');
const { fileURLToPath } = await import('url');
bytes = await fs.readFile(fileURLToPath(wasmURL));
}
result = await WebAssembly.instantiate(bytes, go.importObject);
}
// Run the Go program (it registers globals like pxNewTree, etc.)
// Do not await: the Go WASM main may block (e.g., via select{}), so awaiting never resolves.
go.run(result.instance);
const api = {
// Core functions
version: async () => call('pxVersion'),
hello: async (name) => call('pxHello', name ?? ''),
newTree: async (dim) => {
const info = call('pxNewTree', dim);
return new PxTree(info.treeId);
},
// Statistics utilities
computeDistributionStats: async (distances) => call('pxComputeDistributionStats', distances),
// NAT routing / peer quality functions
computePeerQualityScore: async (metrics, weights) => call('pxComputePeerQualityScore', metrics, weights),
computeTrustScore: async (metrics) => call('pxComputeTrustScore', metrics),
getDefaultQualityWeights: async () => call('pxGetDefaultQualityWeights'),
getDefaultPeerFeatureRanges: async () => call('pxGetDefaultPeerFeatureRanges'),
normalizePeerFeatures: async (features, ranges) => call('pxNormalizePeerFeatures', features, ranges),
weightedPeerFeatures: async (normalized, weights) => call('pxWeightedPeerFeatures', normalized, weights),
// DNS tools
getExternalToolLinks: async (domain) => call('pxGetExternalToolLinks', domain),
getExternalToolLinksIP: async (ip) => call('pxGetExternalToolLinksIP', ip),
getExternalToolLinksEmail: async (emailOrDomain) => call('pxGetExternalToolLinksEmail', emailOrDomain),
getRDAPServers: async () => call('pxGetRDAPServers'),
buildRDAPDomainURL: async (domain) => call('pxBuildRDAPDomainURL', domain),
buildRDAPIPURL: async (ip) => call('pxBuildRDAPIPURL', ip),
buildRDAPASNURL: async (asn) => call('pxBuildRDAPASNURL', asn),
getDNSRecordTypes: async () => call('pxGetDNSRecordTypes'),
getDNSRecordTypeInfo: async () => call('pxGetDNSRecordTypeInfo'),
getCommonDNSRecordTypes: async () => call('pxGetCommonDNSRecordTypes')
};
return api;
}