]> git.codecow.com Git - nano25519.git/commitdiff
Start refactoring worker to simplify setup and teardown.
authorChris Duncan <chris@codecow.com>
Sat, 15 Aug 2026 00:39:06 +0000 (17:39 -0700)
committerChris Duncan <chris@codecow.com>
Sat, 15 Aug 2026 00:39:06 +0000 (17:39 -0700)
index.html
src/lib/worker.ts

index 29a56ac1d6798ce4f7a6502e5e1a65a9004330df..a4c7061a5d450ac61368b7d8aa8070dd127a767b 100644 (file)
@@ -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')
index 522d0a6923e07d6588a5adeedaa7bb54685cbcda..f62fb4e47f89f6cb6502a8a8a65b116394051476 100644 (file)
@@ -1,14 +1,15 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! 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<typeof nano25519_init>) => {
        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<typeof nano2
                        || !('data' in message)
                        || message.data == null
                        || typeof message.data !== 'object'
-                       || !('url' in message.data)
-                       || typeof message.data.url !== 'string'
                        || !('id' in message.data)
                        || typeof message.data.id !== 'string'
                        || !('action' in message.data)
                        || typeof message.data.action !== 'string'
                ) return
                let result: undefined | boolean | string | Uint8Array<ArrayBuffer>
-               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<typeof nano2
                                result = JSON.stringify(err)
                        }
                } finally {
-                       BROWSER: postMessage({ url, id, result })
-                       NODE: host?.postMessage({ data: { url, id, result } })
+                       const data = { id, result }
+                       BROWSER: postMessage(data)
+                       NODE: host?.postMessage({ data })
                }
        }
        BROWSER: addEventListener('message', handleMessage)
        NODE: {
                if (host == null) {
                        import('node:worker_threads')
-                               .then(({ parentPort, threadId }): void => {
+                               .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<unknown> | undefined
 let tasks: Map<string, Parameters<ConstructorParameters<PromiseConstructor>[0]>> = new Map()
 let worker: Worker | NodeWorker
 let url: string
@@ -127,9 +123,10 @@ function isBytes (a: unknown): a is Uint8Array<ArrayBuffer> {
 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<string, any> & Record<'action', Action>, transfer?: ArrayBuffer[]): void {
+       data.id = id
+       BROWSER: worker.postMessage(data, transfer)
+       NODE: worker.postMessage({ data }, transfer)
 }
 
-function onresult (msg: { data: Record<string, unknown> }): void {
+// Parse and validate worker message
+function report (msg: { data: Record<string, unknown> }): 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<string, unknown> }): 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<void> {
-       if (!isWorkerReady) init()
-       if (!isWorkerListening) {
-               return new Promise(async (resolve, reject): Promise<void> => {
-                       const onstarted = (msg: { data: Record<string, unknown> }): 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<unknown> {
+       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<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
+async function dispatch (data: Record<'action', Action> & Record<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
        const id = crypto.randomUUID()
        const transfer: ArrayBuffer[] = []
        for (let k of Object.keys(data)) {
@@ -222,46 +199,24 @@ async function dispatch (data: Record<string, string | ArrayBuffer | Uint8Array<
                }
        }
        console.log('sending data to worker')
-       data.url = url
-       data.id = id
        return new Promise((resolve, reject) => {
                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<void> {
-       return new Promise((resolve, reject): void => {
-               const onstop = (msg: { data: Record<string, unknown> }): 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<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
@@ -270,6 +225,7 @@ export async function run (data: Record<'action', 'verify'> & Record<string, str
 export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean | string | Uint8Array<ArrayBuffer>> {
        try {
                await start()
+               if (url) URL.revokeObjectURL(url)
        } catch (err: any) {
                throw new Error('Error initializing worker')
        }