68bc7015fd
CI / Lint, typecheck, test, build (pull_request) Failing after 9s
- 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.
114 lines
3.2 KiB
TypeScript
114 lines
3.2 KiB
TypeScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import { Bridge } from '../src/bridge.js';
|
|
import type { KRPCState } from '../src/convert.js';
|
|
import { createServer, type Server } from 'node:http';
|
|
|
|
function mockState(ut: number): KRPCState {
|
|
return {
|
|
ut,
|
|
bodies: [],
|
|
vessels: [],
|
|
groundStations: [],
|
|
};
|
|
}
|
|
|
|
describe('Bridge end-to-end', () => {
|
|
let server: Server;
|
|
let received: object[] = [];
|
|
let port: number;
|
|
|
|
beforeAll(async () => {
|
|
server = createServer((req, res) => {
|
|
let body = '';
|
|
req.on('data', (c) => (body += c));
|
|
req.on('end', () => {
|
|
try {
|
|
received.push(JSON.parse(body));
|
|
res.writeHead(200, { 'content-type': 'application/json' });
|
|
res.end(JSON.stringify({ error: false, data: { ok: true } }));
|
|
} catch (e) {
|
|
res.writeHead(400);
|
|
res.end('bad');
|
|
}
|
|
});
|
|
});
|
|
await new Promise<void>((resolve) => server.listen(0, '127.0.0.1', resolve));
|
|
port = (server.address() as { port: number }).port;
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
});
|
|
|
|
it('POSTs snapshots to the API at the configured interval', async () => {
|
|
received = [];
|
|
let counter = 0;
|
|
const bridge = new Bridge({
|
|
apiUrl: `http://127.0.0.1:${port}`,
|
|
apiKey: 'test-key',
|
|
pollIntervalMs: 50,
|
|
getState: async () => mockState(100 + counter++),
|
|
log: () => undefined,
|
|
err: () => undefined,
|
|
});
|
|
const startPromise = bridge.start();
|
|
await new Promise((r) => setTimeout(r, 300));
|
|
bridge.stop();
|
|
await startPromise;
|
|
|
|
expect(received.length).toBeGreaterThan(2);
|
|
expect(received[0]).toMatchObject({
|
|
ut: 100,
|
|
capturedAt: expect.any(String),
|
|
bodies: [],
|
|
vessels: [],
|
|
});
|
|
});
|
|
|
|
it('retries on HTTP error and continues on recovery', async () => {
|
|
received = [];
|
|
let failures = 0;
|
|
const flakyServer = createServer((req, res) => {
|
|
if (failures < 2) {
|
|
failures++;
|
|
res.writeHead(503);
|
|
res.end('down');
|
|
} else {
|
|
let body = '';
|
|
req.on('data', (c) => (body += c));
|
|
req.on('end', () => {
|
|
received.push(JSON.parse(body));
|
|
res.writeHead(200);
|
|
res.end('ok');
|
|
});
|
|
}
|
|
});
|
|
await new Promise<void>((resolve) => flakyServer.listen(0, '127.0.0.1', resolve));
|
|
const flakyPort = (flakyServer.address() as { port: number }).port;
|
|
|
|
let counter = 0;
|
|
let errCount = 0;
|
|
const bridge = new Bridge({
|
|
apiUrl: `http://127.0.0.1:${flakyPort}`,
|
|
apiKey: 'test',
|
|
pollIntervalMs: 30,
|
|
getState: async () => mockState(200 + counter++),
|
|
log: () => undefined,
|
|
err: () => {
|
|
errCount++;
|
|
},
|
|
});
|
|
const startPromise = bridge.start();
|
|
await new Promise((r) => setTimeout(r, 500));
|
|
bridge.stop();
|
|
await startPromise;
|
|
|
|
expect(received.length).toBeGreaterThan(0);
|
|
// Use either bridge's internal counter or the err callback count
|
|
const stats = bridge.getStats();
|
|
expect(stats.failureCount + errCount).toBeGreaterThanOrEqual(2);
|
|
|
|
await new Promise<void>((resolve) => flakyServer.close(() => resolve()));
|
|
});
|
|
});
|