diff options
| author | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
|---|---|---|
| committer | Elvis Claros Castro <elvis@claros.ar> | 2026-09-26 19:26:33 -0300 |
| commit | d80640d01eeb84907e36ff4af33add9081eda17c (patch) | |
| tree | 27698b34e06e28d111c2196700015d9c82f78274 /src/rpc.js | |
| download | wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.tar.gz wa-cli-d80640d01eeb84907e36ff4af33add9081eda17c.zip | |
Diffstat (limited to 'src/rpc.js')
| -rw-r--r-- | src/rpc.js | 83 |
1 files changed, 83 insertions, 0 deletions
diff --git a/src/rpc.js b/src/rpc.js new file mode 100644 index 0000000..d8502e9 --- /dev/null +++ b/src/rpc.js @@ -0,0 +1,83 @@ +// CLI <-> daemon protocol: one JSON object per line over a Unix socket. +// +// -> {"id":1,"method":"messages.list","params":{...}} +// <- {"id":1,"result":...} normal reply +// <- {"id":1,"error":{"code":"EAUTH","message":"..."}} +// <- {"id":1,"event":{...}} stream methods (events.watch), until closed +// +// Simple on purpose: any language can speak it (e.g. a Python bridge with +// socket.AF_UNIX) without going through the `wa` binary. + +import net from 'node:net'; + +export class WaError extends Error { + constructor(code, message, data) { + super(message); + this.code = code; + if (data !== undefined) this.data = data; + } +} + +export function onLines(socket, fn) { + let buf = ''; + socket.setEncoding('utf8'); + socket.on('data', (chunk) => { + buf += chunk; + let i; + while ((i = buf.indexOf('\n')) >= 0) { + const line = buf.slice(0, i); + buf = buf.slice(i + 1); + if (line.trim()) fn(line); + } + }); +} + +export function send(socket, obj) { + if (!socket.destroyed && socket.writable) socket.write(JSON.stringify(obj) + '\n'); +} + +// Calls a method. With onEvent(event, close), it keeps listening for events until +// either side closes. +export function call(socketPath, method, params = {}, { onEvent } = {}) { + return new Promise((resolve, reject) => { + const sock = net.createConnection(socketPath); + let settled = false; + const done = (fn, v) => { + if (settled) return; + settled = true; + fn(v); + }; + sock.on('connect', () => send(sock, { id: 1, method, params })); + onLines(sock, (line) => { + let msg; + try { + msg = JSON.parse(line); + } catch { + return done(reject, new WaError('EINTERNAL', `invalid reply from the daemon: ${line.slice(0, 200)}`)); + } + if (msg.event !== undefined) { + onEvent?.(msg.event, () => sock.end()); + } else if (msg.error) { + done(reject, new WaError(msg.error.code, msg.error.message, msg.error.data)); + sock.end(); + } else { + done(resolve, msg.result); + sock.end(); + } + }); + sock.on('error', (e) => done(reject, e)); + sock.on('close', () => done(onEvent ? resolve : reject, + onEvent ? undefined : new WaError('EDAEMON', 'the daemon closed the connection'))); + }); +} + +export function ping(socketPath) { + return new Promise((resolve) => { + const sock = net.createConnection(socketPath); + sock.on('connect', () => { + sock.destroy(); + resolve(true); + }); + sock.on('error', () => resolve(false)); + }); +} |