1Password / op-js

A JS library powered by the 1Password CLI
https://developer.1password.com/docs/cli
MIT License
91 stars 8 forks source link

Support for async mode #155

Open basert opened 1 year ago

basert commented 1 year ago

Summary

Currently all calls to the op-cli are using spawnSync to block until op-cli has a result. This causes issues when you don't want to block the event loop. Is there planned support for async execution (with Promise as result)?

Proposed solution

Add an async version of https://github.com/1Password/op-js/blob/main/src/cli.ts#L255, using spawn and returning an Promise. Provide an async package of https://github.com/1Password/op-js/blob/main/src/index.ts that returns a Promise instead.

jodyheavener commented 12 months ago

Thanks for filing @basert - this seems like a great idea. I'm slating it for review.

dreusel commented 6 months ago

Just as a hint, here is what I do to wrap calls to op-js in a Promise-wrapped Worker thread: op-cli-worker.js:

'use strict';

const {
    parentPort, workerData,
} = require('node:worker_threads');

const [fName, args] = workerData;
const opJs = require('@1password/op-js');

// Resolve string in the form of 'vault.list' to the actual function
const theFunction = fName.split('.').reduce((o, i) => o[i], opJs);
if (typeof theFunction !== 'function') {
    parentPort.postMessage({error: new Error(`Function ${fName} not found`)});
}

try {
    const result = theFunction(...args);
    parentPort.postMessage({result});
} catch (error) {
    parentPort.postMessage({error});
}

And in your application:

function wrapOpJs(f, ...args) {
    return new Promise((resolve, reject) => {
        const worker = new worker_threads.Worker(`${__dirname}/op-cli-worker.js`, {
            workerData: [f, args],
            env: worker_threads.SHARE_ENV,
        });
        worker.on('message', ({error, result}) => {
            if (error) {
                reject(error);
            } else {
                resolve(result);
            }
        });
        worker.on('error', cause => {
            reject(new Error('1password worker failed', cause));
        });
        worker.on('exit', code => {
            if (code !== 0) {
                reject(new Error(`Worker stopped with exit code ${code}`));
            }
        });
    });
}

Instead of the usual

require('@1password/op-js').item.edit(item.id, fields, flags)

I do:

await wrapOpJs('item.edit', item.id, fields, flags);

[edit: Put the worker code in a separate file as the original way using a stringified function had some weird side-effects]