aboutsummaryrefslogtreecommitdiffstats
path: root/src/rpc.js
blob: d8502e9b81d867f5dc98469160216b5d060088a4 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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));
    });
}