7 Commits

Author SHA1 Message Date
Arnike b09fe6fd99 Merge pull request 'fix: null-safe kRPC handshake error reporting' (#5) from debug-krpc-handshake into main
CI / Lint, typecheck, test, build (push) Failing after 9s
Reviewed-on: #5
2026-06-02 23:12:50 +00:00
Mavis a69cd14817 fix: null-safe kRPC handshake error reporting
CI / Lint, typecheck, test, build (pull_request) Failing after 11s
The bridge was falling back to mock mode with a confusing
'Cannot read properties of null (reading code)' error. The actual
underlying error (ECONNREFUSED, timeout, protocol mismatch) was
being swallowed by our error handler that did `(e as Error).message`
on a value that was sometimes null.

Wrap the kRPC client connect() in per-step try/catch with a
formatErr() helper that handles:
- null / undefined
- strings
- Error with .code (NodeJS.ErrnoException)
- arbitrary objects (JSON.stringify fallback)

Now when the bridge can't reach kRPC you get a real error like
'kRPC RPC TCP connect to 127.0.0.1:50000 failed: code=ECONNREFUSED:
connect ECONNREFUSED 127.0.0.1:50000' instead of the cryptic
null-code message.

Also fixed the bridge's main() error handler to be null-safe.

Discovered while debugging the user's first end-to-end run on
Windows: kRPC was reachable (Test-NetConnection succeeded) but
the bridge couldn't complete the handshake. With this fix we'll
see the real failure mode on the next attempt.
2026-06-02 23:10:19 +00:00
Mavis 6cdd00fdfc Merge phase-1c-extract: typed kRPC service client + SpaceCenter extract
CI / Lint, typecheck, test, build (push) Failing after 10s
Adds the last piece for real-KSP support:
- packages/krpc-client: types, decoder (primitives/classes/enums/collections), services cache, KrpcServices client (invokes by name with auto-encode/decode)
- apps/tools/ksp-bridge/extract.ts: full SpaceCenter extract (~280 procedure calls per poll for a stock save)
- ksp/README.md: complete setup guide + procedure list + troubleshooting
2026-06-02 22:15:23 +00:00
Arnike bd1943510e Merge pull request 'Phase 1c: real kRPC bridge (full protocol + mock mode for development)' (#4) from phase-1c into main
CI / Lint, typecheck, test, build (push) Failing after 10s
Reviewed-on: #4
2026-06-02 20:48:01 +00:00
Arnike b1feea3e6b Merge pull request 'Phase 2c: eclipse/overpass calculators + live-map camera polish' (#3) from phase-2c into main
CI / Lint, typecheck, test, build (push) Failing after 9s
Reviewed-on: #3
2026-06-02 20:47:42 +00:00
Arnike 1e1a940346 Merge pull request 'Phase 2: 3D live map driven by API WebSocket' (#2) from phase-2 into main
CI / Lint, typecheck, test, build (push) Failing after 9s
Reviewed-on: #2
2026-06-02 19:48:39 +00:00
Arnike 10b5927ecc Merge pull request 'Phase 1: data pipeline (Postgres+Redis with in-memory fallback, mock telemetry publisher, WebSocket fan-out, hub /debug page)' (#1) from phase-1 into main
CI / Lint, typecheck, test, build (push) Failing after 9s
Reviewed-on: #1
2026-06-02 19:02:27 +00:00
2 changed files with 60 additions and 16 deletions
+2 -1
View File
@@ -60,7 +60,8 @@ async function main(): Promise<void> {
await adapter.connect();
log(`connected to kRPC at ${HOST}:${RPC_PORT} — running with real KSP state`);
} catch (e) {
log(`no kRPC server at ${HOST}:${RPC_PORT}: ${(e as Error).message}`);
const msg = e === null ? 'null' : e instanceof Error ? e.message : String(e);
log(`no kRPC server at ${HOST}:${RPC_PORT}: ${msg}`);
log('Falling back to MOCK mode (synthetic state).');
return runMock();
}
+58 -15
View File
@@ -38,6 +38,32 @@ export interface ProcedureCallRequest {
type StreamHandler = (streamId: number, result: Uint8Array) => void;
export type { StreamHandler };
/**
* Format an error value for human consumption. Handles the cases where
* the thrown value is null, undefined, a string, or an Error with
* .code (NodeJS.ErrnoException). Falls back to JSON.stringify for
* unknown shapes.
*/
function formatErr(e: unknown): string {
if (e === null) return 'null';
if (e === undefined) return 'undefined';
if (typeof e === 'string') return e;
if (typeof e === 'object') {
const obj = e as { code?: unknown; message?: unknown; errno?: unknown };
const parts: string[] = [];
if (typeof obj.code === 'string') parts.push(`code=${obj.code}`);
if (typeof obj.errno === 'number') parts.push(`errno=${obj.errno}`);
if (typeof obj.message === 'string') parts.push(obj.message);
if (parts.length > 0) return parts.join(': ');
try {
return JSON.stringify(e);
} catch {
return String(e);
}
}
return String(e);
}
export class KRPCClient {
private opts: Required<KRPCClientOptions>;
private rpcSocket: net.Socket | null = null;
@@ -59,11 +85,17 @@ export class KRPCClient {
async connect(): Promise<void> {
// RPC handshake
this.rpcSocket = await tcpConnect(
this.opts.host,
this.opts.rpcPort,
this.opts.connectTimeoutMs,
);
try {
this.rpcSocket = await tcpConnect(
this.opts.host,
this.opts.rpcPort,
this.opts.connectTimeoutMs,
);
} catch (e) {
throw new Error(
`kRPC RPC TCP connect to ${this.opts.host}:${this.opts.rpcPort} failed: ${formatErr(e)}`,
);
}
// The ConnectionRequest.Type enum has RPC = 0, STREAM = 1. We pass
// the numeric value directly because the nested-enum name lookup
// is brittle across protobufjs versions when the enum is nested
@@ -72,11 +104,16 @@ export class KRPCClient {
type: 0, // RPC
clientName: this.opts.clientName,
});
const resp = decodeMessage<{
status: number | string;
message: string;
clientIdentifier: Uint8Array;
}>(KRPC.ConnectionResponse, await recvRawMessage(this.rpcSocket));
let resp: { status: number | string; message: string; clientIdentifier: Uint8Array };
try {
resp = decodeMessage<{
status: number | string;
message: string;
clientIdentifier: Uint8Array;
}>(KRPC.ConnectionResponse, await recvRawMessage(this.rpcSocket));
} catch (e) {
throw new Error(`kRPC RPC handshake (response decode) failed: ${formatErr(e)}`);
}
// protobufjs decodes enums to numbers by default; OK == 0
if (resp.status !== 'OK' && resp.status !== 0) {
throw new Error(`RPC handshake failed: ${resp.status} ${resp.message}`);
@@ -84,11 +121,17 @@ export class KRPCClient {
this.clientIdentifier = Buffer.from(resp.clientIdentifier);
// Stream handshake
this.streamSocket = await tcpConnect(
this.opts.host,
this.opts.streamPort,
this.opts.connectTimeoutMs,
);
try {
this.streamSocket = await tcpConnect(
this.opts.host,
this.opts.streamPort,
this.opts.connectTimeoutMs,
);
} catch (e) {
throw new Error(
`kRPC Stream TCP connect to ${this.opts.host}:${this.opts.streamPort} failed: ${formatErr(e)}`,
);
}
sendMessage(this.streamSocket, KRPC.ConnectionRequest, {
type: 1, // STREAM
clientIdentifier: this.clientIdentifier,