]> git.codecow.com Git - nano25519.git/commitdiff
Extract worker to separate file.
authorChris Duncan <chris@zoso.dev>
Thu, 13 Aug 2026 06:06:43 +0000 (23:06 -0700)
committerChris Duncan <chris@zoso.dev>
Thu, 13 Aug 2026 06:06:43 +0000 (23:06 -0700)
src/async.ts
src/lib/nano25519.ts
src/lib/worker.ts [new file with mode: 0644]

index 181856547af7a69052e0baf3215f4268fc819752..d7eb04436f0330cc8a5f983d0b90a7551c330c63 100644 (file)
@@ -1,7 +1,7 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import { run } from './lib/nano25519'
+import { run } from './lib/worker'
 
 /**
  * Asynchronous Nano public key derivation using WebAssembly.
index ff2fc6090b984063565a8a11d7eeb3080acbf5f5..8fd0115d6130c76480605ddc09d577e214565359 100644 (file)
@@ -1,20 +1,9 @@
 //! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
 //! SPDX-License-Identifier: GPL-3.0-or-later
 
-import { MessagePort as NodeMessagePort, Worker as NodeWorker } from 'node:worker_threads'
 //@ts-expect-error
 import nano25519_wasm from '../../build/nano25519.wasm'
 
-type Data = {
-       url: string
-       id: string
-       action: string
-       message?: string | ArrayBuffer
-       privateKey?: string
-       publicKey?: string
-       secretKey?: string
-       signature?: string
-}
 type Exports = {
        exports: {
                derive: () => void,
@@ -27,7 +16,7 @@ type Exports = {
        }
 }
 
-const nano25519_init = (bytes: number[]): { derive: typeof derive, sign: typeof sign, verify: typeof verify } => {
+export const nano25519_init = (bytes: number[]): { derive: typeof derive, sign: typeof sign, verify: typeof verify } => {
        const wasm: Uint8Array<ArrayBuffer> = Uint8Array.from(bytes)
        const module = new WebAssembly.Module(wasm)
        const { exports } = new WebAssembly.Instance(module, {
@@ -217,293 +206,3 @@ const nano25519_init = (bytes: number[]): { derive: typeof derive, sign: typeof
 }
 
 export const nano25519 = nano25519_init(nano25519_wasm)
-
-const nano25519_worker_init = ({ derive, sign, verify }: typeof nano25519) => {
-       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
-        * by functions in `async` module.
-        * @param {object} message.data - Worker commands and related data
-        */
-       function handleMessage (message: unknown): void {
-               NODE: if (host == null) return queueMicrotask(() => handleMessage(message))
-               if (message == null
-                       || typeof message !== 'object'
-                       || !('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
-
-                       if (data.action === 'start') {
-                               isListening = true
-                               result = 'started'
-                       } else if (data.action === 'stop') {
-                               isListening = false
-                               result = 'stopped'
-                       } else if (isListening) {
-                               const { action } = data
-                               if (action === 'derive') {
-                                       const { privateKey } = data
-                                       const publicKey = derive(privateKey)
-                                       if (publicKey == null) {
-                                               throw new TypeError('Invalid public key from WASM derive()')
-                                       }
-                                       result = publicKey
-                               } else if (action === 'sign') {
-                                       const { message, secretKey } = data
-                                       const signature = sign(message, secretKey)
-                                       if (signature == null) {
-                                               throw new TypeError('Invalid signature from WASM sign()')
-                                       }
-                                       result = signature
-                               } else if (action === 'verify') {
-                                       const { message, publicKey, signature } = data
-                                       const verification = verify(signature, message, publicKey)
-                                       if (verification == null) {
-                                               throw new TypeError('Invalid verification from WASM verify()')
-                                       }
-                                       result = verification
-                               }
-                       }
-               } catch (err: unknown) {
-                       if (typeof err === 'object' && err != null) {
-                               const { message } = err as { [k: string]: unknown }
-                               if (typeof message === 'string' && message !== 'divide by zero') {
-                                       result = message
-                               }
-                       } else {
-                               result = JSON.stringify(err)
-                       }
-               } finally {
-                       BROWSER: postMessage({ url, id, result })
-                       NODE: host?.postMessage({ data: { url, id, result } })
-               }
-       }
-       BROWSER: addEventListener('message', handleMessage)
-       NODE: {
-               if (host == null) {
-                       import('node:worker_threads')
-                               .then(({ parentPort, threadId }): void => {
-                                       host = parentPort
-                                       client = threadId.toString()
-                                       host?.on('message', handleMessage)
-                               })
-               }
-       }
-}
-
-const nano25519_worker = `;(${nano25519_worker_init})((${nano25519_init})([${nano25519_wasm}]));`
-
-/**
- * Host code for asynchronous Web Worker
- */
-let isWorkerReady: boolean = false
-let isWorkerListening: boolean = false
-let tasks: Map<string, Parameters<ConstructorParameters<PromiseConstructor>[0]>> = new Map()
-let worker: Worker | NodeWorker
-let url: string
-
-function isBytes (a: unknown): a is Uint8Array<ArrayBuffer> {
-       return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer
-}
-
-// Create worker module
-function init (): void {
-       try {
-               BROWSER: {
-                       url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
-                       worker = new Worker(url, { type: 'module' })
-               }
-               NODE: {
-                       worker = new NodeWorker(nano25519_worker, {
-                               eval: true,
-                               stderr: false,
-                               stdout: false
-                       })
-                       url = worker.threadId.toString()
-               }
-               console.log(`nano25519 initialized.`)
-               isWorkerReady = true
-       } catch (err: any) {
-               isWorkerReady = false
-               throw new Error('nano25519 initialization failed')
-       }
-}
-
-// Reconstruct worker when errors occur
-function reset (): void {
-       console.warn(`nano25519 encountered an error. Reinitializing...`)
-       isWorkerReady = false
-       worker.terminate()
-       init()
-}
-
-function onresult (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
-       const [ok, err] = executor
-
-       const { result } = data
-       console.log('received result from worker')
-       if (typeof result !== 'boolean' && typeof result !== 'string' && !isBytes(result)) {
-               err('Invalid return type')
-       }
-       ok(result)
-}
-
-// 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 })
-                       }
-               })
-       }
-}
-
-// Send command and relevant data to nano25519 worker
-async function dispatch (data: Record<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
-       const id = crypto.randomUUID()
-       const transfer: ArrayBuffer[] = []
-       for (let k of Object.keys(data)) {
-               if (isBytes(data[k])) {
-                       data[k] = data[k].buffer.slice()
-                       transfer.push(data[k])
-               }
-       }
-       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) }
-       })
-}
-
-// 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' } })
-               }
-       })
-}
-
-export async function run (data: Record<'action', 'derive'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
-export async function run (data: Record<'action', 'sign'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
-export async function run (data: Record<'action', 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean>
-export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean | string | Uint8Array<ArrayBuffer>> {
-       try {
-               await start()
-       } catch (err: any) {
-               throw new Error('Error initializing worker')
-       }
-       try {
-               const result = await dispatch(data)
-               switch (data.action) {
-                       case 'derive': {
-                               if ((isBytes(result) && result.byteLength === 32) || (typeof result === 'string' && /^[0-9a-f]{64}$/i.test(result))) {
-                                       return result
-                               } else {
-                                       throw new Error('derive result invalid')
-                               }
-                       }
-                       case 'sign': {
-                               if ((isBytes(result) && result.byteLength === 64) || (typeof result === 'string' && /^[0-9a-f]{128}$/i.test(result))) {
-                                       return result
-                               } else {
-                                       throw new Error('sign result invalid')
-                               }
-                       }
-                       case 'verify': {
-                               if (typeof result === 'boolean') {
-                                       return result
-                               } else {
-                                       throw new Error('verify result invalid')
-                               }
-                       }
-               }
-       } catch (err: any) {
-               try {
-                       await stop()
-               } catch (e: any) {
-                       console.error('failed to stop worker')
-                       reset()
-               } finally {
-                       throw new Error('Error dispatching async request')
-               }
-       }
-}
diff --git a/src/lib/worker.ts b/src/lib/worker.ts
new file mode 100644 (file)
index 0000000..dea84e4
--- /dev/null
@@ -0,0 +1,309 @@
+//! SPDX-FileCopyrightText: 2026 Chris Duncan <chris@codecow.com>
+//! SPDX-License-Identifier: GPL-3.0-or-later
+
+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 Data = {
+       url: string
+       id: string
+       action: string
+       message?: string | ArrayBuffer
+       privateKey?: string
+       publicKey?: string
+       secretKey?: string
+       signature?: string
+}
+
+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
+        * by functions in `async` module.
+        * @param {object} message.data - Worker commands and related data
+        */
+       function handleMessage (message: unknown): void {
+               NODE: if (host == null) return queueMicrotask(() => handleMessage(message))
+               if (message == null
+                       || typeof message !== 'object'
+                       || !('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
+
+                       if (data.action === 'start') {
+                               isListening = true
+                               result = 'started'
+                       } else if (data.action === 'stop') {
+                               isListening = false
+                               result = 'stopped'
+                       } else if (isListening) {
+                               const { action } = data
+                               if (action === 'derive') {
+                                       const { privateKey } = data
+                                       const publicKey = derive(privateKey)
+                                       if (publicKey == null) {
+                                               throw new TypeError('Invalid public key from WASM derive()')
+                                       }
+                                       result = publicKey
+                               } else if (action === 'sign') {
+                                       const { message, secretKey } = data
+                                       const signature = sign(message, secretKey)
+                                       if (signature == null) {
+                                               throw new TypeError('Invalid signature from WASM sign()')
+                                       }
+                                       result = signature
+                               } else if (action === 'verify') {
+                                       const { message, publicKey, signature } = data
+                                       const verification = verify(signature, message, publicKey)
+                                       if (verification == null) {
+                                               throw new TypeError('Invalid verification from WASM verify()')
+                                       }
+                                       result = verification
+                               }
+                       }
+               } catch (err: unknown) {
+                       if (typeof err === 'object' && err != null) {
+                               const { message } = err as { [k: string]: unknown }
+                               if (typeof message === 'string' && message !== 'divide by zero') {
+                                       result = message
+                               }
+                       } else {
+                               result = JSON.stringify(err)
+                       }
+               } finally {
+                       BROWSER: postMessage({ url, id, result })
+                       NODE: host?.postMessage({ data: { url, id, result } })
+               }
+       }
+       BROWSER: addEventListener('message', handleMessage)
+       NODE: {
+               if (host == null) {
+                       import('node:worker_threads')
+                               .then(({ parentPort, threadId }): void => {
+                                       host = parentPort
+                                       client = threadId.toString()
+                                       host?.on('message', handleMessage)
+                               })
+               }
+       }
+}
+
+const nano25519_worker = `;(${nano25519_worker_init})((${nano25519_init})([${nano25519_wasm}]));`
+
+/**
+ * Host code for asynchronous Web Worker
+ */
+let isWorkerReady: boolean = false
+let isWorkerListening: boolean = false
+let tasks: Map<string, Parameters<ConstructorParameters<PromiseConstructor>[0]>> = new Map()
+let worker: Worker | NodeWorker
+let url: string
+
+function isBytes (a: unknown): a is Uint8Array<ArrayBuffer> {
+       return a instanceof Uint8Array && a.buffer instanceof ArrayBuffer
+}
+
+// Create worker module
+function init (): void {
+       try {
+               BROWSER: {
+                       url = URL.createObjectURL(new Blob([nano25519_worker], { type: 'text/javascript' }))
+                       worker = new Worker(url, { type: 'module' })
+               }
+               NODE: {
+                       worker = new NodeWorker(nano25519_worker, {
+                               eval: true,
+                               stderr: false,
+                               stdout: false
+                       })
+                       url = worker.threadId.toString()
+               }
+               console.log(`nano25519 initialized.`)
+               isWorkerReady = true
+       } catch (err: any) {
+               isWorkerReady = false
+               throw new Error('nano25519 initialization failed')
+       }
+}
+
+// Reconstruct worker when errors occur
+function reset (): void {
+       console.warn(`nano25519 encountered an error. Reinitializing...`)
+       isWorkerReady = false
+       worker.terminate()
+       init()
+}
+
+function onresult (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
+       const [ok, err] = executor
+
+       const { result } = data
+       console.log('received result from worker')
+       if (typeof result !== 'boolean' && typeof result !== 'string' && !isBytes(result)) {
+               err('Invalid return type')
+       }
+       ok(result)
+}
+
+// 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 })
+                       }
+               })
+       }
+}
+
+// Send command and relevant data to nano25519 worker
+async function dispatch (data: Record<string, string | ArrayBuffer | Uint8Array<ArrayBuffer>>): Promise<unknown> {
+       const id = crypto.randomUUID()
+       const transfer: ArrayBuffer[] = []
+       for (let k of Object.keys(data)) {
+               if (isBytes(data[k])) {
+                       data[k] = data[k].buffer.slice()
+                       transfer.push(data[k])
+               }
+       }
+       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) }
+       })
+}
+
+// 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' } })
+               }
+       })
+}
+
+export async function run (data: Record<'action', 'derive'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
+export async function run (data: Record<'action', 'sign'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<string | Uint8Array<ArrayBuffer>>
+export async function run (data: Record<'action', 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean>
+export async function run (data: Record<'action', 'derive' | 'sign' | 'verify'> & Record<string, string | Uint8Array<ArrayBuffer>>): Promise<boolean | string | Uint8Array<ArrayBuffer>> {
+       try {
+               await start()
+       } catch (err: any) {
+               throw new Error('Error initializing worker')
+       }
+       try {
+               const result = await dispatch(data)
+               switch (data.action) {
+                       case 'derive': {
+                               if ((isBytes(result) && result.byteLength === 32) || (typeof result === 'string' && /^[0-9a-f]{64}$/i.test(result))) {
+                                       return result
+                               } else {
+                                       throw new Error('derive result invalid')
+                               }
+                       }
+                       case 'sign': {
+                               if ((isBytes(result) && result.byteLength === 64) || (typeof result === 'string' && /^[0-9a-f]{128}$/i.test(result))) {
+                                       return result
+                               } else {
+                                       throw new Error('sign result invalid')
+                               }
+                       }
+                       case 'verify': {
+                               if (typeof result === 'boolean') {
+                                       return result
+                               } else {
+                                       throw new Error('verify result invalid')
+                               }
+                       }
+               }
+       } catch (err: any) {
+               try {
+                       await stop()
+               } catch (e: any) {
+                       console.error('failed to stop worker')
+                       reset()
+               } finally {
+                       throw new Error('Error dispatching async request')
+               }
+       }
+}