Files
stack/packages/bus/src/client.mjs
T
jason.woltje 23e504f331 fix(cli,bus,organization): E2E6 lane f6c, refusal lines name the next step (#1873)
1. Stale bus host lock refusals from bus status, start, stop and restart
   name mosaic bus recover ORG.
2. org definitions: unknown org, unknown or malformed name and bad id give
   one refusal or usage line, no stack trace, no host path.
3. Pack, board and workflow refusals print their hint as template save does;
   tested with the real CliError transport shape.
4. console start and stop clear a console/start.lock whose named supervisor
   (pid and start time) is gone; a live or unnamed holder refuses.
5. Template save keeps free text verbatim and writes no placeholders; the
   preview lists each field that mentions an org-specific name.
6. A request the shim never sent (no socket, connect refused) exits 5 and
   the CLI reports bus-not-running with the start and recover line.
7. autonomy-precondition carries the check that failed, including on the
   hire paths.
8. Saving under an existing template name refuses template-exists with a
   next step; refusal table entry added, channel-proof pins moved.
2026-11-13 20:14:21 -06:00

65 lines
2.9 KiB
JavaScript

import { connect } from 'node:net';
import { BusError, PROJECT_SAVE_VERB, PROJECT_SAVE_REQUEST_MAX } from './broker.mjs';
// E2E6 fix 6: a connect that fails with one of these never sent the request. The code stays
// outcome-unknown for every caller that reads it (the harness, the Console), and the error is marked
// notSent so the human and tester shims can exit with NOT_SENT_EXIT and `mosaic` can say the bus is
// not running.
const NOT_SENT = new Set(['ENOENT', 'ECONNREFUSED', 'ENOTDIR', 'ENOTSOCK']);
export const NOT_SENT_EXIT = 5;
export class Client {
constructor({ path, cap, human, tester, user, console: owner, timeout = 5000 }) {
this.path = path;
this.cap = cap;
this.human = human;
this.tester = tester;
this.user = user;
this.console = owner;
this.timeout = timeout;
}
call(verb, args = {}) {
const request =
JSON.stringify({
...(this.human ? { human: this.human } : this.tester ? { tester: this.tester } : this.user ? { user: this.user } : this.console ? { console: this.console } : { cap: this.cap }),
verb,
args,
}) + '\n';
if (Buffer.byteLength(request) > ([PROJECT_SAVE_VERB, 'library.file.save'].includes(verb) ? PROJECT_SAVE_REQUEST_MAX : 65536))
return Promise.reject(new BusError('request-too-large'));
return new Promise((resolve, reject) => {
const socket = connect(this.path);
let input = Buffer.alloc(0),
settled = false,
connected = false;
const end = (error, value) => {
if (settled) return;
settled = true;
socket.destroy();
error ? reject(error) : resolve(value);
};
socket.setTimeout(['processing.halt', 'processing.resume'].includes(verb) ? Math.max(this.timeout, 20000) : this.timeout, () => end(new BusError('outcome-unknown')));
socket.on('connect', () => { connected = true; socket.write(request); });
socket.on('error', (error) => {
const unknown = new BusError('outcome-unknown');
if (!connected && NOT_SENT.has(error?.code)) unknown.notSent = true;
end(unknown);
});
socket.on('end', () => end(new BusError('outcome-unknown')));
socket.on('data', (b) => {
input = Buffer.concat([input, b]);
if (input.length > 4 * 1024 * 1024) return end(new BusError('response-too-large'));
if (!input.includes(10)) return;
try {
const r = JSON.parse(input.toString('utf8'));
if (r.ok === true) end(null, r.result);
else if (r.ok === false && typeof r.error === 'string' && /^[a-z-]{1,64}$/.test(r.error))
// Preserve a bounded reason when an explicitly scoped refusal has one.
end(new BusError(r.error, typeof r.reason === 'string' ? r.reason.slice(0, 300) : undefined));
else end(new BusError('invalid-response'));
} catch {
end(new BusError('invalid-response'));
}
});
});
}
}