From f386a6c0ab7c5a68382f3fb194cb0f521845a5fa Mon Sep 17 00:00:00 2001
From: Chris Duncan <chris@zoso.dev>
Date: Sat, 16 Nov 2024 13:12:31 -0800
Subject: [PATCH] Scrap worker pool in favor of one worker per wallet.

---
 package.json                     |  2 +-
 src/lib/ckd.ts                   | 16 +++++---
 src/lib/pool.ts                  | 65 --------------------------------
 src/lib/safe.ts                  | 12 +++---
 src/lib/wallet.ts                | 40 ++++++++++++--------
 test/{GLOBALS.js => GLOBALS.mjs} | 10 ++---
 test/create-wallet.test.mjs      |  2 +-
 test/derive-accounts.test.mjs    | 14 +++----
 test/import-wallet.test.mjs      | 14 +++----
 test/lock-unlock-wallet.mjs      |  2 +-
 10 files changed, 63 insertions(+), 114 deletions(-)
 delete mode 100644 src/lib/pool.ts
 rename test/{GLOBALS.js => GLOBALS.mjs} (67%)

diff --git a/package.json b/package.json
index d46f9d6..fc3ad00 100644
--- a/package.json
+++ b/package.json
@@ -39,7 +39,7 @@
 		"url": "git+https://zoso.dev/libnemo.git"
 	},
 	"scripts": {
-		"build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js ckd=dist/lib/ckd.js --outdir=dist --target=es2022 --format=esm --platform=browser --bundle --minify --sourcemap",
+		"build": "rm -rf dist && tsc && esbuild main.min=dist/main.js global.min=dist/global.js ckd=dist/lib/ckd.js --outdir=dist --target=es2022 --format=esm --platform=node --bundle --minify --sourcemap",
 		"test": "npm run build && node --test --env-file .env",
 		"test:coverage": "npm run test -- --experimental-test-coverage",
 		"test:coverage:report": "npm run test:coverage -- --test-reporter=lcov --test-reporter-destination=coverage.info && genhtml coverage.info --output-directory test/coverage && rm coverage.info && xdg-open test/coverage/index.html"
diff --git a/src/lib/ckd.ts b/src/lib/ckd.ts
index 3338d49..377aacb 100644
--- a/src/lib/ckd.ts
+++ b/src/lib/ckd.ts
@@ -1,11 +1,17 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import { Account } from './account.js'
 import { nanoCKD } from './bip32-key-derivation.js'
 import { bytes, dec } from './convert.js'
 import Tools from './tools.js'
-import type { Ledger } from './ledger.js'
+
+if (globalThis.Worker == null) {
+	const { isMainThread, parentPort } = await import('node:worker_threads')
+	if (!isMainThread) {
+		globalThis.addEventListener ??= Object.getPrototypeOf(parentPort).on.bind(parentPort)
+		globalThis.postMessage ??= Object.getPrototypeOf(parentPort).postMessage.bind(parentPort)
+	}
+}
 
 /**
 * Derives BIP-44 Nano account private keys.
@@ -13,8 +19,8 @@ import type { Ledger } from './ledger.js'
 * @param {number} index - Index of the account
 * @returns {Promise<Account>}
 */
-globalThis.onmessage = (event) => {
-	const { type, seed, index } = event.data
+addEventListener('message', (message) => {
+	const { type, seed, index } = message.data ?? message
 	switch (type) {
 		case 'bip44': {
 			ckdBip44(seed, index).then(postMessage)
@@ -25,7 +31,7 @@ globalThis.onmessage = (event) => {
 			break
 		}
 	}
-}
+}, { once: true })
 
 /**
 * Derives BIP-44 Nano account private keys.
diff --git a/src/lib/pool.ts b/src/lib/pool.ts
deleted file mode 100644
index 6d4690f..0000000
--- a/src/lib/pool.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-// SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
-// SPDX-License-Identifier: GPL-3.0-or-later
-const Worker = globalThis.Worker
-
-type Task = {
-	data: object,
-	resolve: Function
-}
-
-class Thread {
-	isAvailable: boolean
-	worker: Worker
-
-	constructor (url: string | URL) {
-		this.isAvailable = true
-		this.worker = new Worker(new URL(url, import.meta.url))
-	}
-}
-
-/**
-* Assigns a Web Worker to process data. Creates a new one if none are available.
-*
-* @param {object} data - Arbitrary data encapsulated in a JSON object
-* @param {string} signature - Hexadcimal-formatted signature
-* @param {...string} input - Data to be verified
-* @returns {boolean} True if the data was signed by the public key's matching private key
-*/
-export class Pool {
-	#tasks: any[]
-	#threads: Thread[]
-	#url
-
-	constructor (url: string | URL) {
-		this.#tasks = []
-		this.#url = new URL(url, import.meta.url)
-		this.#threads = Array(navigator.hardwareConcurrency)
-			.fill(undefined)
-			.map(() => { return new Thread(this.#url) })
-	}
-
-	#assign (thread: Thread, task: Task) {
-		thread.isAvailable = false
-		thread.worker.onmessage = (event) => {
-			if (this.#tasks.length > 0) {
-				const next = this.#tasks.shift()
-				this.#assign(thread, next)
-			} else {
-				thread.isAvailable = true
-			}
-			task.resolve(event.data)
-		}
-		thread.worker.postMessage(task.data)
-	}
-
-	async work (data: object): Promise<any> {
-		return new Promise(resolve => {
-			const thread = this.#threads.find(t => t.isAvailable)
-			if (thread) {
-				this.#assign(thread, { data, resolve })
-			} else {
-				this.#tasks.push({ data, resolve })
-			}
-		})
-	}
-}
diff --git a/src/lib/safe.ts b/src/lib/safe.ts
index cebab59..8654756 100644
--- a/src/lib/safe.ts
+++ b/src/lib/safe.ts
@@ -4,10 +4,10 @@
 import { buffer, hex, utf8 } from './convert.js'
 import { Entropy } from './entropy.js'
 const { subtle } = globalThis.crypto
-const storage = globalThis.sessionStorage
 const ERR_MSG = 'Failed to store item in Safe'
 
 export class Safe {
+	#storage = globalThis.sessionStorage
 
 	/**
 	* Encrypts data with a password and stores it in the Safe.
@@ -18,7 +18,7 @@ export class Safe {
 	*/
 	async put (name: string, key: CryptoKey, data: any): Promise<boolean>
 	async put (name: string, passkey: string | CryptoKey, data: any): Promise<boolean> {
-		if (storage.getItem(name)) {
+		if (this.#storage.getItem(name)) {
 			throw new Error(ERR_MSG)
 		}
 		return this.overwrite(name, passkey as string, data)
@@ -60,7 +60,7 @@ export class Safe {
 			}
 			await new Promise<void>((resolve, reject) => {
 				try {
-					storage.setItem(name, JSON.stringify(record))
+					this.#storage.setItem(name, JSON.stringify(record))
 					resolve()
 				} catch (err) {
 					reject(err)
@@ -70,7 +70,7 @@ export class Safe {
 		} catch (err) {
 			throw new Error(ERR_MSG)
 		}
-		return (storage.getItem(name) != null)
+		return (this.#storage.getItem(name) != null)
 	}
 
 	/**
@@ -87,7 +87,7 @@ export class Safe {
 		}
 
 		const item = await new Promise<string | null>(resolve => {
-			resolve(storage.getItem(name))
+			resolve(this.#storage.getItem(name))
 		})
 		if (item == null) {
 			return null
@@ -110,7 +110,7 @@ export class Safe {
 			const decoded = buffer.toUtf8(decrypted)
 			const data = JSON.parse(decoded)
 			passkey = ''
-			storage.removeItem(name)
+			this.#storage.removeItem(name)
 			return data
 		} catch (err) {
 			return null
diff --git a/src/lib/wallet.ts b/src/lib/wallet.ts
index bebb491..8d8fa80 100644
--- a/src/lib/wallet.ts
+++ b/src/lib/wallet.ts
@@ -3,18 +3,12 @@
 
 import { Account } from './account.js'
 import { Bip39Mnemonic } from './bip39-mnemonic.js'
-import { nanoCKD } from './bip32-key-derivation.js'
 import { ADDRESS_GAP, SEED_LENGTH_BIP44, SEED_LENGTH_BLAKE2B } from './constants.js'
-import { bytes, dec } from './convert.js'
 import { Entropy } from './entropy.js'
-import { Pool } from './pool.js'
 import { Rpc } from './rpc.js'
 import { Safe } from './safe.js'
-import Tools from './tools.js'
 import type { Ledger } from './ledger.js'
 
-const ckdPool = new Pool('./ckd.js')
-
 /**
 * Represents a wallet containing numerous Nano accounts derived from a single
 * source, the form of which can vary based on the type of wallet. The Wallet
@@ -236,12 +230,14 @@ abstract class Wallet {
 */
 export class Bip44Wallet extends Wallet {
 	static #isInternal: boolean = false
+	#worker: Worker
 
 	constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {
 		if (!Bip44Wallet.#isInternal) {
 			throw new Error(`Bip44Wallet cannot be instantiated directly. Use 'await Bip44Wallet.create()' instead.`)
 		}
 		super(seed, mnemonic, id)
+		this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' })
 		Bip44Wallet.#isInternal = false
 	}
 
@@ -393,11 +389,16 @@ export class Bip44Wallet extends Wallet {
 	* @returns {Promise<Account>}
 	*/
 	async ckd (index: number): Promise<Account> {
-		const key = await ckdPool.work({ type: 'bip44', seed: this.seed, index })
-		if (typeof key !== 'string') {
-			throw new TypeError('BIP-44 child key derivation returned invalid data')
-		}
-		return await Account.fromPrivateKey(key, index)
+		return new Promise(resolve => {
+			this.#worker.addEventListener('message', (message) => {
+				const key = message.data ?? message
+				if (typeof key !== 'string') {
+					throw new TypeError('BIP-44 child key derivation returned invalid data')
+				}
+				Account.fromPrivateKey(key, index).then(resolve)
+			}, { once: true })
+			this.#worker.postMessage({ type: 'bip44', seed: this.seed, index })
+		})
 	}
 }
 
@@ -419,12 +420,14 @@ export class Bip44Wallet extends Wallet {
 */
 export class Blake2bWallet extends Wallet {
 	static #isInternal: boolean = false
+	#worker: Worker
 
 	constructor (seed: string, mnemonic?: Bip39Mnemonic, id?: string) {
 		if (!Blake2bWallet.#isInternal) {
 			throw new Error(`Blake2bWallet cannot be instantiated directly. Use 'await Blake2bWallet.create()' instead.`)
 		}
 		super(seed, mnemonic, id)
+		this.#worker = new Worker(new URL('./ckd.js', import.meta.url), { type: 'module' })
 		Blake2bWallet.#isInternal = false
 	}
 
@@ -538,11 +541,16 @@ export class Blake2bWallet extends Wallet {
 	* @returns {Promise<Account>}
 	*/
 	async ckd (index: number): Promise<Account> {
-		const key = await ckdPool.work({ type: 'blake2b', seed: this.seed, index })
-		if (typeof key !== 'string') {
-			throw new TypeError('BLAKE2b child key derivation returned invalid data')
-		}
-		return await Account.fromPrivateKey(key, index)
+		return new Promise(resolve => {
+			this.#worker.addEventListener('message', (message) => {
+				const key = message.data ?? message
+				if (typeof key !== 'string') {
+					throw new TypeError('BLAKE2b child key derivation returned invalid data')
+				}
+				Account.fromPrivateKey(key, index).then(resolve)
+			}, { once: true })
+			this.#worker.postMessage({ type: 'blake2b', seed: this.seed, index })
+		})
 	}
 }
 
diff --git a/test/GLOBALS.js b/test/GLOBALS.mjs
similarity index 67%
rename from test/GLOBALS.js
rename to test/GLOBALS.mjs
index fa1af78..0ef5e2f 100644
--- a/test/GLOBALS.js
+++ b/test/GLOBALS.mjs
@@ -1,11 +1,11 @@
 // SPDX-FileCopyrightText: 2024 Chris Duncan <chris@zoso.dev>
 // SPDX-License-Identifier: GPL-3.0-or-later
 
-import { Worker } from 'node:worker_threads'
-
-globalThis.Worker ??= Worker
-globalThis.Worker.onmessage ??= (handler) => Worker.prototype.on('message', handler)
-globalThis.onmessage ??= (handler) => EventTarget.prototype.addEventListener('message', handler)
+if (globalThis.Worker == null) {
+	const { Worker } = await import('node:worker_threads')
+	Worker.prototype.addEventListener = Worker.prototype.on
+	globalThis.Worker = Worker
+}
 
 if (globalThis.sessionStorage == null) {
 	const _sessionStorage = {}
diff --git a/test/create-wallet.test.mjs b/test/create-wallet.test.mjs
index b9e576f..26b8e77 100644
--- a/test/create-wallet.test.mjs
+++ b/test/create-wallet.test.mjs
@@ -3,7 +3,7 @@
 
 'use strict'
 
-import './GLOBALS.js'
+import './GLOBALS.mjs'
 import { describe, it } from 'node:test'
 import { strict as assert } from 'assert'
 import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'
diff --git a/test/derive-accounts.test.mjs b/test/derive-accounts.test.mjs
index 076a560..49a4a47 100644
--- a/test/derive-accounts.test.mjs
+++ b/test/derive-accounts.test.mjs
@@ -3,7 +3,7 @@
 
 'use strict'
 
-import './GLOBALS.js'
+import './GLOBALS.mjs'
 import { describe, it } from 'node:test'
 import { strict as assert } from 'assert'
 import { NANO_TEST_VECTORS } from './TEST_VECTORS.js'
@@ -17,7 +17,7 @@ describe('derive child accounts from the same seed', async function () {
 	const wallet = await Bip44Wallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, NANO_TEST_VECTORS.BIP39_SEED)
 	await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)
 
-	await it('should derive the first account from the given BIP-44 seed', async function () {
+	it('should derive the first account from the given BIP-44 seed', async function () {
 		const accounts = await wallet.accounts()
 
 		assert.equal(accounts.length, 1)
@@ -26,7 +26,7 @@ describe('derive child accounts from the same seed', async function () {
 		assert.equal(accounts[0].address, NANO_TEST_VECTORS.ADDRESS_0)
 	})
 
-	await it('should derive low indexed accounts from the given BIP-44 seed', async function () {
+	it('should derive low indexed accounts from the given BIP-44 seed', async function () {
 		const accounts = await wallet.accounts(1, 2)
 
 		assert.equal(accounts.length, 2)
@@ -38,7 +38,7 @@ describe('derive child accounts from the same seed', async function () {
 		assert.equal(accounts[1].address, NANO_TEST_VECTORS.ADDRESS_2)
 	})
 
-	await it('should derive high indexed accounts from the given seed', async function () {
+	it('should derive high indexed accounts from the given seed', async function () {
 		const accounts = await wallet.accounts(0x70000000, 0x700000ff)
 
 		assert.equal(accounts.length, 0x100)
@@ -51,7 +51,7 @@ describe('derive child accounts from the same seed', async function () {
 		}
 	})
 
-	await it('should derive accounts for a BLAKE2b wallet', async function () {
+	it('should derive accounts for a BLAKE2b wallet', async function () {
 		const bwallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)
 		await bwallet.unlock(NANO_TEST_VECTORS.PASSWORD)
 		const lowAccounts = await bwallet.accounts(0, 2)
@@ -91,7 +91,7 @@ describe('Ledger device accounts', { skip: true }, async () => {
 })
 
 describe('child key derivation performance', { skip }, async () => {
-	await it('performance test of BIP-44 ckd', async function () {
+	it('performance test of BIP-44 ckd', async function () {
 		const wallet = await Bip44Wallet.create(NANO_TEST_VECTORS.PASSWORD)
 		await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)
 
@@ -100,7 +100,7 @@ describe('child key derivation performance', { skip }, async () => {
 		assert.equal(accounts.length, 0x8000)
 	})
 
-	await it('performance test of BLAKE2b ckd', async function () {
+	it('performance test of BLAKE2b ckd', async function () {
 		const wallet = await Blake2bWallet.create(NANO_TEST_VECTORS.PASSWORD)
 		await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)
 
diff --git a/test/import-wallet.test.mjs b/test/import-wallet.test.mjs
index 91f1d07..ba3cbd5 100644
--- a/test/import-wallet.test.mjs
+++ b/test/import-wallet.test.mjs
@@ -3,7 +3,7 @@
 
 'use strict'
 
-import './GLOBALS.js'
+import './GLOBALS.mjs'
 import { describe, it } from 'node:test'
 import { strict as assert } from 'assert'
 import { BIP32_TEST_VECTORS, CUSTOM_TEST_VECTORS, NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'
@@ -139,8 +139,8 @@ describe('import wallet with test vectors test', () => {
 	})
 
 	it('should successfully import a BLAKE2b wallet with Trezor test vectors', async () => {
-		const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)
-		await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)
+		const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_1)
+		await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)
 		const accounts = await wallet.accounts(0, 1)
 
 		assert.ok('mnemonic' in wallet)
@@ -160,8 +160,8 @@ describe('import wallet with test vectors test', () => {
 	})
 
 	it('should get identical BLAKE2b wallets when created with a seed versus with its derived mnemonic', async () => {
-		const wallet = await Blake2bWallet.fromSeed(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)
-		await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)
+		const wallet = await Blake2bWallet.fromSeed(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.ENTROPY_2)
+		await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)
 		const walletAccounts = await wallet.accounts()
 		const walletAccount = walletAccounts[0]
 
@@ -182,8 +182,8 @@ describe('import wallet with test vectors test', () => {
 	})
 
 	it('should get identical BLAKE2b wallets when created with max entropy value', async () => {
-		const wallet = await Blake2bWallet.fromMnemonic(TREZOR_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)
-		await wallet.unlock(TREZOR_TEST_VECTORS.PASSWORD)
+		const wallet = await Blake2bWallet.fromMnemonic(NANO_TEST_VECTORS.PASSWORD, TREZOR_TEST_VECTORS.MNEMONIC_3)
+		await wallet.unlock(NANO_TEST_VECTORS.PASSWORD)
 		const accounts = await wallet.accounts()
 
 		assert.ok('mnemonic' in wallet)
diff --git a/test/lock-unlock-wallet.mjs b/test/lock-unlock-wallet.mjs
index e09f1d9..a67089f 100644
--- a/test/lock-unlock-wallet.mjs
+++ b/test/lock-unlock-wallet.mjs
@@ -3,7 +3,7 @@
 
 'use strict'
 
-import './GLOBALS.js'
+import './GLOBALS.mjs'
 import { describe, it } from 'node:test'
 import { strict as assert } from 'assert'
 import { NANO_TEST_VECTORS, TREZOR_TEST_VECTORS } from './TEST_VECTORS.js'
-- 
2.34.1