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));
});
}
|