]> git.codecow.com Git - libnemo.git/commitdiff
Refactor vault tasks to use map of IDs instead of a serial queue. Improve worker...
authorChris Duncan <chris@codecow.com>
Fri, 26 Jun 2026 21:58:15 +0000 (14:58 -0700)
committerChris Duncan <chris@codecow.com>
Fri, 26 Jun 2026 21:58:15 +0000 (14:58 -0700)
src/lib/vault/index.ts
src/lib/vault/vault-worker.ts

index dd80441b1ecd426bc395f67984c3ef696da0fbd8..e020c39038133181de88bb84b61440b9f550e369 100644 (file)
@@ -7,7 +7,7 @@ import { Data } from '../database'
 type TaskData = {
        [key: string]: Data | Record<string, Data>
        url: string
-       id: number
+       id: string
 }
 
 type Task = {
@@ -27,7 +27,7 @@ export class Vault {
        #job?: Task
        #isLocked: boolean = true
        #isTerminated: boolean = false
-       #queue: Task[] = []
+       #tasks: Map<string, Task> = new Map<string, Task>()
        #url: string
        #worker: NodeWorker | Worker
 
@@ -37,20 +37,24 @@ export class Vault {
        removeEventListener = this.#eventTarget.removeEventListener.bind(this.#eventTarget)
 
        constructor () {
-               BROWSER: this.#url = URL.createObjectURL(new Blob([vaultWorker], { type: 'text/javascript' }))
-               BROWSER: this.#worker = new Worker(this.#url, { type: 'module' })
-               BROWSER: this.#worker.addEventListener('message', message => {
+               const listener = (message: MessageEvent<any>) => {
+                       console.log('host listener(message)', message, message?.data)
                        this.#report(message.data)
-               })
-               NODE: this.#worker = new NodeWorker(vaultWorker, {
-                       eval: true,
-                       stderr: false,
-                       stdout: false
-               })
-               NODE: this.#worker.on('message', message => {
-                       this.#report(message)
-               })
-               NODE: this.#url = this.#worker.threadId.toString()
+               }
+               BROWSER: {
+                       this.#url = URL.createObjectURL(new Blob([vaultWorker], { type: 'text/javascript' }))
+                       this.#worker = new Worker(this.#url, { type: 'module' })
+                       this.#worker.addEventListener('message', listener)
+               }
+               NODE: {
+                       this.#worker = new NodeWorker(vaultWorker, {
+                               eval: true,
+                               stderr: false,
+                               stdout: false
+                       })
+                       this.#url = this.#worker.threadId.toString()
+                       this.#worker.on('message', listener)
+               }
        }
 
        get isLocked (): boolean { return this.#isLocked }
@@ -59,10 +63,17 @@ export class Vault {
                if (this.#isTerminated) {
                        throw new Error(TERMINATED)
                }
+               const buffers: ArrayBuffer[] = []
+               for (const d of Object.values(payload)) {
+                       if (d instanceof ArrayBuffer) {
+                               buffers.push(d)
+                       }
+               }
+               const id = crypto.randomUUID()
                const data: TaskData = {
                        ...payload,
                        url: this.#url,
-                       id: performance.now(),
+                       id
                }
                return new Promise((resolve, reject): void => {
                        const task: Task = {
@@ -70,8 +81,14 @@ export class Vault {
                                resolve,
                                reject
                        }
-                       this.#queue.push(task)
-                       if (this.#job == null) this.#process()
+                       this.#tasks.set(id, task)
+                       try {
+                               console.log('host postMessage(data)', data)
+                               BROWSER: this.#worker.postMessage(data, buffers)
+                               NODE: this.#worker.postMessage({ data }, buffers)
+                       } catch (err) {
+                               reject(err)
+                       }
                })
        }
 
@@ -83,61 +100,32 @@ export class Vault {
                NODE: this.#worker.unref()
                this.#job?.reject(TERMINATED)
                this.#job = undefined
-               for (const task of this.#queue) {
+               for (const [_, task] of this.#tasks) {
                        task?.reject?.(TERMINATED)
                }
-               this.#queue = []
+               this.#tasks.clear()
        }
 
-       #process = (): void => {
-               this.#job = this.#queue.shift()
-               if (this.#job != null) {
-                       const { data, reject } = this.#job
-                       const buffers: ArrayBuffer[] = []
-                       for (const d of Object.values(data)) {
-                               if (d instanceof ArrayBuffer) {
-                                       buffers.push(d)
-                               }
+       #report (results: unknown): void {
+               if (results == null) return
+               const { url, id, error, isLocked } = results as Record<string, unknown>
+               if (url === this.#url) {
+                       if (typeof id !== 'string') {
+                               throw new Error('Vault worker job invalid ID')
                        }
-                       try {
-                               BROWSER: this.#worker.postMessage(data, buffers)
-                               NODE: this.#worker.postMessage({ data }, buffers)
-                       } catch (err) {
-                               reject(err)
-                       }
-               }
-       }
-
-       #report (results: any): void {
-               if (results === LOCKED || results === UNLOCKED) {
-                       const isLocked = results === LOCKED
-                       if (this.#isLocked !== isLocked) {
+                       if (typeof isLocked === 'boolean' && this.#isLocked !== isLocked) {
                                this.#isLocked = isLocked
-                               this.dispatchEvent(new Event(results))
+                               this.dispatchEvent(new Event(isLocked ? LOCKED : UNLOCKED))
                        }
-                       return
-               }
-               if (this.#job == null) {
-                       throw new Error('Vault worker returned results without an associated job.')
-               }
-               const { data: { url, id }, resolve, reject } = this.#job
-               if (url == null) {
-                       throw new Error('Vault worker job missing URL')
-               }
-               if (id == null) {
-                       throw new Error('Vault worker job missing ID')
-               }
-               if (results?.url === url && results?.id === id) {
-                       try {
-                               if (results?.error != null) {
-                                       reject(results)
-                               } else {
-                                       resolve(results)
-                               }
-                       } catch (err) {
-                               reject(err)
-                       } finally {
-                               this.#process()
+                       const task = this.#tasks.get(id)
+                       if (task == null) {
+                               throw new Error('Vault worker returned results without an associated job.')
+                       }
+                       const { resolve, reject } = task
+                       if (error != null) {
+                               reject(results)
+                       } else {
+                               resolve(results)
                        }
                }
        }
index 8ad0c25fddbe40ae41d87b00d059314421c6694e..3573311184d0f0d474a6cb0a4673c5d7d999a345 100644 (file)
@@ -29,7 +29,7 @@ const listener = (event: MessageEvent<any>): void => {
        }
        const data = event.data as Record<string, unknown>
        const { url, id } = data
-       if (typeof id !== 'number' && typeof id !== 'string') return
+       if (typeof id !== 'string') return
        BROWSER: if (url !== location.href) return
        NODE: if (url !== threadId.toString()) return
        NODE: if (parentPort == null) setTimeout(() => listener(event), 0)
@@ -70,7 +70,7 @@ const listener = (event: MessageEvent<any>): void => {
                                        return update(key, keySalt)
                                }
                                case 'verify': {
-                                       return Promise.resolve(verify(seed, mnemonicPhrase))
+                                       return verify(seed, mnemonicPhrase)
                                }
                                default: {
                                        throw new Error(`Unknown wallet action '${action}'`)
@@ -91,8 +91,8 @@ const listener = (event: MessageEvent<any>): void => {
                        result.id = id
                        console.log('worker postMessage(result)', result)
                        //@ts-expect-error
-                       BROWSER: postMessage(result, transfer)
-                       NODE: parentPort?.postMessage(result, transfer)
+                       BROWSER: self.postMessage(result, transfer)
+                       NODE: parentPort?.postMessage({ data: result }, transfer)
                })
                .catch((err: any) => {
                        for (let data of Object.values(event.data)) {
@@ -102,11 +102,11 @@ const listener = (event: MessageEvent<any>): void => {
                                data = undefined
                        }
                        console.log('worker postMessage(error)', err)
-                       BROWSER: postMessage({ url, id, error: 'Failed to process Vault request', cause: err })
-                       NODE: parentPort?.postMessage({ url, id, error: 'Failed to process Vault request', cause: err })
+                       BROWSER: self.postMessage({ url, id, error: 'Failed to process Vault request', cause: err })
+                       NODE: parentPort?.postMessage({ data: { url, id, error: 'Failed to process Vault request', cause: err } })
                })
 }
-BROWSER: addEventListener('message', listener)
+BROWSER: self.addEventListener('message', listener)
 NODE: parentPort?.on('message', listener)
 
 /**
@@ -220,14 +220,12 @@ async function load (type?: WalletType, key?: CryptoKey, keySalt?: ArrayBuffer,
                .finally(() => lock())
 }
 
-async function lock (): Promise<void> {
+async function lock (): Promise<Record<string, boolean>> {
        _mnemonic = undefined
        _seed = undefined
        _locked = true
        _timer?.pause()
-       BROWSER: postMessage('locked')
-       NODE: parentPort?.postMessage('locked')
-       return Promise.resolve()
+       return Promise.resolve({ isLocked: true })
 }
 
 /**
@@ -267,15 +265,13 @@ async function sign (index?: number, data?: ArrayBuffer): Promise<Record<string,
 /**
 * Decrypts the input and sets the seed and, if it is included, the mnemonic.
 */
-async function unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<void> {
+async function unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, encrypted?: ArrayBuffer): Promise<Record<string, boolean>> {
        if (type == null) {
                throw new TypeError('Wallet type is required')
        }
        if (type === 'Ledger') {
                _locked = false
-               BROWSER: postMessage('unlocked')
-               NODE: parentPort?.postMessage('unlocked')
-               return Promise.resolve()
+               return Promise.resolve({ isLocked: false })
        }
        if (key == null) {
                throw new TypeError('Wallet password is required')
@@ -300,8 +296,7 @@ async function unlock (type?: WalletType, key?: CryptoKey, iv?: ArrayBuffer, enc
                        _mnemonic = mnemonic
                        _locked = false
                        _timer = new VaultTimer(lock, _timeout)
-                       BROWSER: postMessage('unlocked')
-                       NODE: parentPort?.postMessage('unlocked')
+                       return Promise.resolve({ isLocked: false })
                })
                .catch(err => {
                        console.error(err)
@@ -344,7 +339,7 @@ async function update (key?: CryptoKey, salt?: ArrayBuffer): Promise<Record<stri
 * Checks the seed and, if it exists, the mnemonic against input. The wallet
 * must be unlocked prior to verification.
 */
-function verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record<string, boolean> {
+function verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Promise<Record<string, boolean>> {
        try {
                if (_locked) {
                        throw new Error('Wallet is locked')
@@ -377,7 +372,7 @@ function verify (seed?: ArrayBuffer, mnemonicPhrase?: string): Record<string, bo
                        }
                        isVerified = diff === 0
                }
-               return { isVerified }
+               return Promise.resolve({ isVerified })
        } catch (err) {
                console.error(err)
                throw new Error('Failed to verify wallet', { cause: err })