From: Chris Duncan Date: Sat, 15 Aug 2026 00:39:06 +0000 (-0700) Subject: Start refactoring worker to simplify setup and teardown. X-Git-Url: https://git.codecow.com/?a=commitdiff_plain;h=c368a326aed59a6314a2ab138e2d094c8758e96c;p=nano25519.git Start refactoring worker to simplify setup and teardown. --- diff --git a/index.html b/index.html index 29a56ac..a4c7061 100644 --- a/index.html +++ b/index.html @@ -54,7 +54,7 @@ SPDX-License-Identifier: GPL-3.0-or-later * @param {number[]} times - List of numbers, often timing durations. * @returns Object with averaged values for the specified list. */ - function stats (times) { + function stats(times) { if (times == null || times.length === 0) return null const count = times.length @@ -109,7 +109,7 @@ SPDX-License-Identifier: GPL-3.0-or-later } } - function average (times, type) { + function average(times, type) { const averages = stats(times) const title = `${type} derive-sign-verify` return { @@ -117,7 +117,7 @@ SPDX-License-Identifier: GPL-3.0-or-later } } - function random (length = 32) { + function random(length = 32) { const bytes = crypto.getRandomValues(new Uint8Array(length)) return [...bytes].map(b => b.toString(16).padStart(2, '0')).join('') } @@ -202,7 +202,7 @@ SPDX-License-Identifier: GPL-3.0-or-later } } - async function sequenceDeriveSignVerify (api, blockHash, privateKey) { + async function sequenceDeriveSignVerify(api, blockHash, privateKey) { let start = 0, end = 0, publicKey = null, signature = null, isValid = false if (api === 'nano25519 (async)') { start = performance.now() @@ -226,7 +226,7 @@ SPDX-License-Identifier: GPL-3.0-or-later return end - start } - export async function test (api, size, runs) { + export async function test(api, size, runs) { if (typeof size !== 'number' || size < 1) { size = 1 } @@ -238,7 +238,7 @@ SPDX-License-Identifier: GPL-3.0-or-later * @param {string} name * @param {boolean} test */ - function check (name, test) { + function check(name, test) { if (typeof test !== 'boolean') { throw 'invalid test' } @@ -289,50 +289,50 @@ SPDX-License-Identifier: GPL-3.0-or-later passes += +test failures += +!test - // test both strings and bytes as input - for (const { privateKey, publicKey, message, signature } of PYTHON_ED25519_BLAKE2B_VECTORS) { - result = nano25519.derive(privateKey) - test = result.toLowerCase() === publicKey - check(`derive from ${privateKey}`, test) - passes += +test - failures += +!test - - result = nano25519.sign(message, privateKey + publicKey) - test = result.toLowerCase() === signature.slice(0, 128) - check(`sign message ${message}`, test) - passes += +test - failures += +!test - - result = nano25519.verify(signature.slice(0, 128), message, publicKey) - test = result === true - check(`verify signature ${signature.slice(0, 128)}`, test) - passes += +test - failures += +!test - - const privateKeyBytes = new Uint8Array(privateKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? []) - const publicKeyBytes = new Uint8Array(publicKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? []) - const secretKeyBytes = new Uint8Array([...privateKeyBytes, ...publicKeyBytes]) - const messageBytes = new Uint8Array(message.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? []) - const signatureBytes = new Uint8Array(signature.match(/.{2}/g)?.slice(0, 64).map(b => parseInt(b, 16)) ?? []) - - result = nano25519.derive(privateKeyBytes) - test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === publicKey - check(`derive from ${privateKey}`, test) - passes += +test - failures += +!test - - result = nano25519.sign(messageBytes, secretKeyBytes) - test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === signature.slice(0, 128) - check(`sign message ${message}`, test) - passes += +test - failures += +!test - - result = nano25519.verify(signatureBytes, messageBytes, publicKeyBytes) - test = result === true - check(`verify signature ${signature.slice(0, 128)}`, test) - passes += +test - failures += +!test - } + // // test both strings and bytes as input + // for (const { privateKey, publicKey, message, signature } of PYTHON_ED25519_BLAKE2B_VECTORS) { + // result = nano25519.derive(privateKey) + // test = result.toLowerCase() === publicKey + // check(`derive from ${privateKey}`, test) + // passes += +test + // failures += +!test + + // result = nano25519.sign(message, privateKey + publicKey) + // test = result.toLowerCase() === signature.slice(0, 128) + // check(`sign message ${message}`, test) + // passes += +test + // failures += +!test + + // result = nano25519.verify(signature.slice(0, 128), message, publicKey) + // test = result === true + // check(`verify signature ${signature.slice(0, 128)}`, test) + // passes += +test + // failures += +!test + + // const privateKeyBytes = new Uint8Array(privateKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? []) + // const publicKeyBytes = new Uint8Array(publicKey.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? []) + // const secretKeyBytes = new Uint8Array([...privateKeyBytes, ...publicKeyBytes]) + // const messageBytes = new Uint8Array(message.match(/.{2}/g)?.map(b => parseInt(b, 16)) ?? []) + // const signatureBytes = new Uint8Array(signature.match(/.{2}/g)?.slice(0, 64).map(b => parseInt(b, 16)) ?? []) + + // result = nano25519.derive(privateKeyBytes) + // test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === publicKey + // check(`derive from ${privateKey}`, test) + // passes += +test + // failures += +!test + + // result = nano25519.sign(messageBytes, secretKeyBytes) + // test = [...result].map(b => b.toString(16).padStart(2, '0')).join('').toLowerCase() === signature.slice(0, 128) + // check(`sign message ${message}`, test) + // passes += +test + // failures += +!test + + // result = nano25519.verify(signatureBytes, messageBytes, publicKeyBytes) + // test = result === true + // check(`verify signature ${signature.slice(0, 128)}`, test) + // passes += +test + // failures += +!test + // } // XFAIL try { @@ -421,7 +421,7 @@ SPDX-License-Identifier: GPL-3.0-or-later console.log('%cTESTING COMPLETE', 'color:orange;font-weight:bold') } - function startValidation (event) { + function startValidation(event) { const api = document.getElementById('api')?.value const signature = document.getElementById('signature')?.value const blockHash = document.getElementById('blockHash')?.value @@ -445,7 +445,7 @@ SPDX-License-Identifier: GPL-3.0-or-later document.getElementById('signature').addEventListener('input', startValidation) document.getElementById('publicKey').addEventListener('input', startValidation) - function startTest (event) { + function startTest(event) { event.target.disabled = true const api = document.getElementById('api') const size = document.getElementById('size') diff --git a/src/lib/worker.ts b/src/lib/worker.ts index 522d0a6..f62fb4e 100644 --- a/src/lib/worker.ts +++ b/src/lib/worker.ts @@ -1,14 +1,15 @@ //! SPDX-FileCopyrightText: 2026 Chris Duncan //! SPDX-License-Identifier: GPL-3.0-or-later +import { UUID } from 'node:crypto' import { MessagePort as NodeMessagePort, Worker as NodeWorker } from 'node:worker_threads' //@ts-expect-error import nano25519_wasm from '../../build/nano25519.wasm' - import { nano25519_init } from './nano25519' +type Action = 'derive' | 'sign' | 'start' | 'stop' | 'verify' + type Data = { - url: string id: string action: string message?: string | ArrayBuffer @@ -21,7 +22,6 @@ type Data = { const nano25519_worker_init = ({ derive, sign, verify }: ReturnType) => { let isListening = false let host: NodeMessagePort | null = null - let client: string | undefined = globalThis.location?.href /** * Parses inbound data when nano25519 is started as a Web Worker. Only called @@ -35,20 +35,16 @@ const nano25519_worker_init = ({ derive, sign, verify }: ReturnType - let url: undefined | string let id: undefined | string try { - const data: Data = message.data as object & { url: string, id: string, action: string } - { ({ url, id } = data) } - if (url !== client) return + const data: Data = message.data as object & { id: string, action: string } + id = data.id if (data.action === 'start') { isListening = true @@ -91,17 +87,17 @@ const nano25519_worker_init = ({ derive, sign, verify }: ReturnType { + .then(({ parentPort }): void => { host = parentPort - client = threadId.toString() host?.on('message', handleMessage) }) } @@ -114,7 +110,7 @@ const nano25519_worker = `;(${nano25519_worker_init})((${nano25519_init})([${nan * Host code for asynchronous Web Worker */ let isWorkerReady: boolean = false -let isWorkerListening: boolean = false +let starting: Promise | undefined let tasks: Map[0]>> = new Map() let worker: Worker | NodeWorker let url: string @@ -127,9 +123,10 @@ function isBytes (a: unknown): a is Uint8Array { function init (): void { try { BROWSER: { - if (url) URL.revokeObjectURL(url) url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' })) worker = new Worker(url, { type: 'module' }) + worker.onmessage = report + worker.onerror = reset } NODE: { worker = new NodeWorker(nano25519_worker, { @@ -137,7 +134,8 @@ function init (): void { stderr: false, stdout: false }) - url = worker.threadId.toString() + worker.on('message', report) + worker.on('error', reset) } console.log(`nano25519 initialized.`) isWorkerReady = true @@ -147,23 +145,22 @@ function init (): void { } } -// Reconstruct worker when errors occur -function reset (): void { - console.warn(`nano25519 encountered an error. Reinitializing...`) - isWorkerReady = false - worker.terminate() - init() +// Helper for environment-specific messaging to worker +function post (id: UUID, data: Record & Record<'action', Action>, transfer?: ArrayBuffer[]): void { + data.id = id + BROWSER: worker.postMessage(data, transfer) + NODE: worker.postMessage({ data }, transfer) } -function onresult (msg: { data: Record }): void { +// Parse and validate worker message +function report (msg: { data: Record }): void { const { data } = msg - if (data.url !== url) return if (!('id' in data) || typeof data.id !== 'string') return const executor = tasks.get(data.id) if (executor == null) return - tasks.delete(data.id) const [ok, err] = executor + tasks.delete(data.id) const { result } = data console.log('received result from worker') @@ -173,46 +170,26 @@ function onresult (msg: { data: Record }): void { return ok(result) } +// Reconstruct worker when errors occur +function reset (): void { + console.warn(`nano25519 encountered an error. Reinitializing...`) + isWorkerReady = false + worker.terminate() + init() +} + // Check that the worker is running and listening before sending messages -async function start (): Promise { - if (!isWorkerReady) init() - if (!isWorkerListening) { - return new Promise(async (resolve, reject): Promise => { - const onstarted = (msg: { data: Record }): void => { - if (msg.data.url !== url) return - const { result } = msg.data - if (result === 'started') { - console.log('worker started successfully') - BROWSER: { (worker as Worker).onmessage = onresult } - NODE: { (worker as NodeWorker).on('message', onresult) } - isWorkerListening = true - resolve() - } else { - isWorkerListening = false - reject() - } - } - console.log(`starting worker`) - const id = crypto.randomUUID() - const data = { url, id, action: 'start' } - BROWSER: { - worker = worker as Worker - worker.onerror = reject - worker.onmessage = onstarted - worker.postMessage(data) - } - NODE: { - worker = worker as unknown as NodeWorker - worker.on('error', reject) - worker.on('message', onstarted) - worker.postMessage({ data }) - } - }) - } +async function start (): Promise { + return starting ??= new Promise((resolve, reject): void => { + if (!isWorkerReady) init() + const id = crypto.randomUUID() + tasks.set(id, [resolve, reject]) + post(id, { action: 'start' }) + }) } // Send command and relevant data to nano25519 worker -async function dispatch (data: Record>): Promise { +async function dispatch (data: Record<'action', Action> & Record>): Promise { const id = crypto.randomUUID() const transfer: ArrayBuffer[] = [] for (let k of Object.keys(data)) { @@ -222,46 +199,24 @@ async function dispatch (data: Record { tasks.set(id, [resolve, reject]) console.log('tasks', tasks) - BROWSER: { (worker as Worker).postMessage(data, transfer) } - NODE: { (worker as NodeWorker).postMessage({ data }, transfer) } + post(id, data, transfer) }) } // Request that the worker stop listening without terminating async function stop (): Promise { - return new Promise((resolve, reject): void => { - const onstop = (msg: { data: Record }): void => { - const { data } = msg - if (data.url !== url) return - const { result } = data - if (result === 'stopped') { - console.log('worker stopped successfully') - isWorkerListening = false - resolve() - } else { - reject(result) - } - } - console.log(`stopping worker`) - const id = crypto.randomUUID() - BROWSER: { - worker = worker as Worker - worker.onerror = reject - worker.onmessage = onstop - worker.postMessage({ url, id, action: 'stop' }) - } - NODE: { - worker = worker as unknown as NodeWorker - worker.on('error', reject) - worker.on('message', onstop) - worker.postMessage({ data: { url, id, action: 'stop' } }) - } - }) + console.log('stopping worker') + const result = await dispatch({ action: 'stop' }) + if (result === 'stopped') { + console.log('worker stopped successfully') + } else if (typeof result === 'string') { + throw new Error(result) + } else { + throw new Error('unknown error while stopping worker') + } } export async function run (data: Record<'action', 'derive'> & Record>): Promise> @@ -270,6 +225,7 @@ export async function run (data: Record<'action', 'verify'> & Record & Record>): Promise> { try { await start() + if (url) URL.revokeObjectURL(url) } catch (err: any) { throw new Error('Error initializing worker') }