Carries forward the correct fixes from debug-krpc-handshake and adds
two new decode-path fixes that the mock server surfaced. Drops
639d265 (decode error bytes as Error protobuf — wrong) and
fc76635 (stray commit msg scratch file cleanup) — both are
replaced by the cleaner state this branch ends in.
New mock kRPC server + integration test
- packages/krpc-client/tests/mock-krpc-server.ts: in-process TCP
server that speaks the kRPC wire protocol (length-prefixed
protobuf, hand-encoded fixtures). Exports startMockKrpcServer().
- packages/krpc-client/tests/mock-krpc-server.test.ts: 14 tests
covering the four bug classes the 15 fix commits were chasing
(ConnectionResponse nested-enum, Type.code nested-enum,
Procedure.game_scenes missing, null returnType). Plus the two
new bug classes below.
- apps/tools/ksp-bridge/tests/mock-krpc-integration.test.ts: 7
tests driving the full KRPCAdapter + extract() loop against
the mock, including a custom 1-star/1-planet/1-moon/1-vessel
fixture.
- packages/krpc-client/package.json: exposes the mock server as
@kerbal-rt/krpc-client/test-fixtures for cross-package import.
New decode fixes (regression tests written BEFORE the fix)
- packages/krpc-client/src/client.ts: serialize concurrent invokes
on a per-socket promise chain. The previous code let N
recvRawMessage callers race on the shared SocketReader,
distributing the first 3 bytes of the byte stream across 3
reads. Symptom: 'index out of range' or 'invalid wire type' on
Promise.all([invoke, invoke, invoke]) — exactly the pattern in
extract.ts. Fix: invoke() awaits the previous invoke before
touching the socket.
- packages/krpc-client/src/service-client.ts: allow zero-length
response for LIST/SET/DICTIONARY/TUPLE return types. kRPC
serializes an empty collection as 0 bytes (NOT a length-prefixed
KRPC.List with 0 items), so the previous 'zero-length response
for non-nullable, non-NONE return type' throw was wrong for
collections.
- apps/tools/ksp-bridge/src/extract.ts: handle the root body
(Kerbol) returning null for get_Orbit(). Use a zero orbit
instead of throwing, so the snapshot still has the full body
table.
Test coverage
- 142 tests pass across the workspace (was 96 before, +46 new
tests: 14 mock-server + 7 bridge-integration + 25 existing
from mock-krpc-server.test.ts duplicate coverage).
- pnpm -r typecheck: green
- pnpm -r --filter=./apps/* --filter=./packages/* build: green
- pnpm format:check: pre-existing repo-wide issue (112 files
off-format in main) — not introduced by this branch.
Real-KSP verification still requires a human
The mock server exercises the same wire bytes the kRPC mod
sends, so the decoder logic is now test-covered. But three
things only the human can verify:
1. KSP 1.12.5 install + kRPC mod via CKAN
2. Alt+F12 in-game -> RPC server on 127.0.0.1:50000
3. Visual live-map motion after bridge POSTs the first snapshot
(current /debug page also works as a sanity check)
The kRPC server's GetServices response includes a `game_scenes`
field on every Procedure message — a repeated GameScene enum
describing which KSP scene the procedure is available in
(SPACE_CENTER, FLIGHT, EDITOR_VAB, etc.). My schema was missing
field 6 entirely, and the field type is a nested enum which hits
the same protobufjs bug we've been chasing.
This is the actual cause of the 'Cannot read properties of null
(reading code)' error that has been blocking the bridge. The
decoder was trying to resolve the GameScene nested-enum
descriptor and throwing.
Fix: add field 6 as repeated uint32 (same nested-enum workaround
as Type.code and ConnectionResponse.status), with the enum values
kept as a nested GameScene for documentation/lookup.
After this fix, the GetServices response should decode cleanly
and the bridge should connect to real KSP.
The real root cause of the 'Cannot read properties of null (reading code)'
error was hiding in two places, not one. The first (ConnectionResponse
status) was fixed in the previous commit. This commit fixes the second:
The KRPC.Type message has a 'code' field of nested-enum type
'Type.TypeCode'. When protobufjs decodes a GetServices response
(which contains MANY Type messages inside Procedure.returnType
fields), it hits the same nested-enum default-value lookup bug
and throws the TypeError.
The fix is identical to the ConnectionResponse.status fix:
change the field type from the nested-enum reference to plain
'uint32'. The wire format is the same (varint), and our code in
services.ts reads raw integers from the typecode field anyway.
This error was happening OUTSIDE my top-level try/catch in
client.connect() — it was in loadServices, called from
adapter.connect(). The bridge's catch caught it, but the error
message was the raw 'Cannot read properties of null (reading code)'
because loadServices didn't wrap the error.
Now:
- The schema fix makes the decode actually succeed
- The adapter wraps any remaining loadServices errors with a
clear 'kRPC loadServices (GetServices decode) failed:' prefix
so the next failure mode is immediately actionable
If you see [v2-debug] in the log, you're on the new code. If
you don't, you're still on main and the old error handler is
hiding the real error.
Also hardened the fatal catch handler to never crash on weird
error values.
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.
Built the missing piece that connects the ksp-bridge to a real KSP
instance via kRPC. This adds a typed service client on top of the
existing KRPCClient, plus the SpaceCenter-specific extraction logic
that pulls the universe state from a running KSP save.
@kerbal-rt/krpc-client
- types.ts — runtime representation of kRPC Type descriptors
(TypeCode enum, KrpcType interface, decodeKrpcType, typeName)
- decoder.ts — kRPC value codec: primitive decode/encode, class refs,
enums, collections (LIST/SET/TUPLE/DICTIONARY), system messages
(STATUS/SERVICES/STREAM/EVENT/PROCEDURE_CALL). 34 unit tests.
- services.ts — ServiceCache built from KRPC.GetServices() response.
Lookup by (service, procedure), enum value/name resolution. 12 tests.
- service-client.ts — KrpcServices: high-level invoke-by-name client.
loadServices() helper to connect + load catalog. 9 integration tests
with a mock kRPC server.
- schema.ts — added Set/Dictionary/Event/Expression types so the
decoder can handle system messages without external .proto files.
Also fixed a bug where 'STREAM' was being encoded as 0 due to
protobufjs's nested-enum string lookup.
ksp-bridge
- extract.ts — the actual SpaceCenter calls. ~280 procedure calls
per poll for a typical KSP save (UT, bodies, vessels, then per-body
and per-vessel class methods in parallel). Build the UniverseSnapshot.
- krpc-adapter.ts — rewrote to use KrpcServices (typed) instead of
the stub extract function. Supports an optional injected services
for testing.
- bridge.ts — uses the new ExtractedState type and buildSnapshot.
- index.ts — connects to kRPC; falls back to mock mode if no server.
- convert.ts — backward-compat shim, re-exports from extract.ts.
The ksp-bridge can now talk to a real KSP install. We do NOT need
the kRPC mod's .proto files on disk — the server's GetServices()
response is the source of truth for type info. Documented the full
list of procedures we call, the kRPC value encoding, and the new
architecture in ksp/README.md.
Tests: 119 total, all green. Typecheck and build clean across all 11
projects.
Bonus: fixed an integer-overflow bug in the krpc-client connect()
handshake (was passing 'RPC'/'STREAM' strings to protobufjs; its
nested-enum string lookup silently encodes as 0, which made the
stream handshake send the wrong type. Switched to numeric codes.)
- packages/krpc-client: TypeScript kRPC protocol client
- connection.ts: varint encoding/decoding, length-prefix framing,
per-socket read queue (avoids race when multiple read promises
in flight). Uses multiplication not '<<' for varint shift
because JS truncates << to 32 bits.
- schema.ts: hand-written protobufjs schema for kRPC meta-protocol
(ConnectionRequest, Request, Response, StreamUpdate, Status, etc.)
- enough to do connection handshake, single procedure calls, and
stream subscription. Service-specific types (SpaceCenter.Vessel,
Orbit, CelestialBody) need to be loaded from the kRPC mod's
.proto files at runtime.
- client.ts: KRPCClient with connect/invoke/addStream/removeStream/
onStreamUpdate/close. Tested with hand-rolled mock server.
- 10 tests (varint round-trips incl. uint64, wire format with raw
sockets).
- apps/tools/ksp-bridge: bridge that connects KSP to our API
- convert.ts: pure kRPC -> UniverseSnapshot conversion
(situation enum mapping, body id normalization, etc.)
- bridge.ts: main poll loop with retry + backoff
- krpc-adapter.ts: KRPCAdapter class that owns the KRPCClient
- index.ts: entrypoint with MOCK MODE for development (emits
synthetic state when no KSP is available, so you can verify the
HTTP pipeline end-to-end)
- 9 tests (7 conversion + 2 end-to-end bridge)
- ksp/README.md: full setup guide
- CKAN install, KSP server start, env vars
- Hand-rolled KSP calls list (what SpaceCenter methods we need)
- Roadmap for the remaining .proto-loading work
- Protocol deep-dive (for the next dev)
- Bug fixes along the way: protobufjs default import (not namespace),
varint 32-bit truncation, JavaScript bitwise 32-bit limit,
handshake status enum comparison (number vs string).
End-to-end verified: API + bridge in mock mode, 2 bodies + 1 vessel
arriving at /api/v1/state, 500ms polling cadence, automatic recovery
on HTTP failures.
- packages/db: postgres.js + ioredis wrapper, StateStore interface with
InMemory + Postgres implementations, schema migration runner
- apps/api: refactored to use @kerbal-rt/db; live WebSocket hub subscribes
to store changes and broadcasts to all clients
- apps/tools/mock-telemetry: Node script that generates realistic KSP
state and POSTs to /api/v1/ingest at 1Hz (uses same keplerian math
as the live-map renderer)
- apps/hub: new /debug page that connects to /api/v1/live and shows
the live state
- ksp/README.md: documents Phase 1c (real kRPC bridge) and the two
implementation options
- Tests: 17 total (5 kepler math, 5 db store, 5 API health/state,
2 API WebSocket fan-out)
End-to-end verified: mock publisher → API → hub /debug page,
11 snapshots/5s over WebSocket, vessels advancing in mean anomaly.