Phase 0: monorepo skeleton (hub, live-map, api, packages, infra, CI)
This commit is contained in:
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@kerbal-rt/shared-types",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "TypeScript types shared between API, hub, and live-map",
|
||||
"type": "module",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "echo 'no linter yet'",
|
||||
"test": "echo 'no tests yet'",
|
||||
"build": "tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.6.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
/**
|
||||
* Shared types used across the API, hub, and live-map frontends.
|
||||
*
|
||||
* Everything that crosses an HTTP or WebSocket boundary lives here.
|
||||
* Nothing in this package should import from anywhere else in the
|
||||
* monorepo — keep it a leaf.
|
||||
*/
|
||||
|
||||
// ─── Celestial bodies ──────────────────────────────────────────────────────
|
||||
|
||||
export type BodyKind = 'star' | 'planet' | 'moon' | 'asteroid' | 'comet';
|
||||
|
||||
/**
|
||||
* Keplerian orbital elements at a given epoch.
|
||||
* Angles are in radians, distances in meters.
|
||||
*/
|
||||
export interface KeplerianElements {
|
||||
/** Semi-major axis (m) */
|
||||
semiMajorAxis: number;
|
||||
/** Eccentricity (0=circle, 0..1=ellipse, 1+=parabolic/hyperbolic) */
|
||||
eccentricity: number;
|
||||
/** Inclination (rad) */
|
||||
inclination: number;
|
||||
/** Longitude of ascending node (rad) */
|
||||
longitudeOfAscendingNode: number;
|
||||
/** Argument of periapsis (rad) */
|
||||
argumentOfPeriapsis: number;
|
||||
/** Mean anomaly at epoch (rad) */
|
||||
meanAnomalyAtEpoch: number;
|
||||
/** Epoch (UT seconds) */
|
||||
epoch: number;
|
||||
}
|
||||
|
||||
export interface CelestialBody {
|
||||
id: string;
|
||||
name: string;
|
||||
kind: BodyKind;
|
||||
parentId: string | null;
|
||||
radius: number; // m
|
||||
/** Sphere of influence (m); 0 or Infinity for the system root (the star) */
|
||||
sphereOfInfluence: number;
|
||||
/** Gravitational parameter μ = G·M (m^3/s^2) */
|
||||
gravitationalParameter: number;
|
||||
/** Rotation period (s); 0 for tidally-locked or N/A */
|
||||
rotationPeriod: number;
|
||||
/** Axial tilt (rad) */
|
||||
axialTilt: number;
|
||||
/** Keplerian elements relative to the parent body */
|
||||
orbit: KeplerianElements;
|
||||
}
|
||||
|
||||
// ─── Vessels ───────────────────────────────────────────────────────────────
|
||||
|
||||
export type VesselSituation =
|
||||
| 'ORBITING'
|
||||
| 'ESCAPING'
|
||||
| 'LANDED'
|
||||
| 'SPLASHED'
|
||||
| 'PRELAUNCH'
|
||||
| 'FLYING'
|
||||
| 'SUB_ORBITAL'
|
||||
| 'DOCKED'
|
||||
| 'UNKNOWN';
|
||||
|
||||
export type VesselStatus = 'ACTIVE' | 'DECOMMISSIONED' | 'LOST';
|
||||
|
||||
export interface Vessel {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string | null; // e.g. "Probe", "Booster", "Station"
|
||||
owner: string | null; // e.g. "KASA", "SPES"
|
||||
situation: VesselSituation;
|
||||
status: VesselStatus;
|
||||
/** Keplerian elements relative to the body the vessel is currently orbiting */
|
||||
orbit: KeplerianElements;
|
||||
/** ID of the body the vessel's orbit is currently referenced to */
|
||||
referenceBodyId: string;
|
||||
createdAt: string; // ISO 8601
|
||||
retiredAt: string | null;
|
||||
}
|
||||
|
||||
export interface GroundStation {
|
||||
id: string;
|
||||
name: string;
|
||||
bodyId: string;
|
||||
lat: number; // deg
|
||||
lon: number; // deg
|
||||
alt: number; // m above sea level
|
||||
}
|
||||
|
||||
// ─── State snapshots ───────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* A full universe snapshot, published by the KSP telemetry bridge
|
||||
* on each tick. Frontends use this to render the live map and to
|
||||
* maintain client-side state.
|
||||
*/
|
||||
export interface UniverseSnapshot {
|
||||
/** KSP universal time, seconds from epoch 0 (year 1, day 1) */
|
||||
ut: number;
|
||||
/** Wall-clock time when this snapshot was captured (ISO 8601) */
|
||||
capturedAt: string;
|
||||
activeVesselId: string | null;
|
||||
bodies: CelestialBody[];
|
||||
vessels: Vessel[];
|
||||
groundStations: GroundStation[];
|
||||
}
|
||||
|
||||
// ─── Missions & events ─────────────────────────────────────────────────────
|
||||
|
||||
export type MissionEventKind =
|
||||
| 'LAUNCH'
|
||||
| 'BURN'
|
||||
| 'FLYBY'
|
||||
| 'LANDING'
|
||||
| 'DOCKING'
|
||||
| 'EVA'
|
||||
| 'ECLIPSE'
|
||||
| 'OPERATION'
|
||||
| 'OTHER';
|
||||
|
||||
export interface MissionEvent {
|
||||
id: number;
|
||||
mission: string;
|
||||
vesselId: string | null;
|
||||
title: string;
|
||||
description: string | null;
|
||||
scheduledAt: string; // ISO 8601 wall-clock
|
||||
utAtEvent: number | null; // KSP UT at the scheduled time, if known
|
||||
durationSeconds: number | null;
|
||||
kind: MissionEventKind;
|
||||
createdBy: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export interface Mission {
|
||||
id: string;
|
||||
name: string;
|
||||
agency: string; // "KASA" | "SPES" | other
|
||||
description: string | null;
|
||||
startedAt: string;
|
||||
endedAt: string | null;
|
||||
vesselIds: string[];
|
||||
}
|
||||
|
||||
// ─── Media ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export type MediaKind = 'image' | 'youtube';
|
||||
|
||||
export interface MediaItem {
|
||||
id: number;
|
||||
kind: MediaKind;
|
||||
url: string;
|
||||
thumbnailUrl: string | null;
|
||||
caption: string | null;
|
||||
tags: string[];
|
||||
missionId: string | null;
|
||||
vesselId: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
// ─── API envelope ──────────────────────────────────────────────────────────
|
||||
|
||||
export interface ApiError {
|
||||
error: true;
|
||||
code: string;
|
||||
message: string;
|
||||
details?: unknown;
|
||||
}
|
||||
|
||||
export interface ApiOk<T> {
|
||||
error: false;
|
||||
data: T;
|
||||
}
|
||||
|
||||
export type ApiResponse<T> = ApiOk<T> | ApiError;
|
||||
|
||||
export * from './schemas.js';
|
||||
|
||||
// ─── WebSocket protocol ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Messages pushed from the API to live-map / hub clients over `/api/v1/live`.
|
||||
* Clients should ignore unknown message types (forward-compat).
|
||||
*/
|
||||
export type LiveMessage =
|
||||
| { type: 'snapshot'; snapshot: UniverseSnapshot }
|
||||
| { type: 'vessel_added'; vessel: Vessel }
|
||||
| { type: 'vessel_updated'; vessel: Vessel }
|
||||
| { type: 'vessel_removed'; vesselId: string }
|
||||
| { type: 'event_new'; event: MissionEvent }
|
||||
| { type: 'event_updated'; event: MissionEvent }
|
||||
| { type: 'event_removed'; eventId: number }
|
||||
| { type: 'mission_update'; mission: Mission }
|
||||
| { type: 'ping'; serverTime: string };
|
||||
|
||||
// ─── Constants ─────────────────────────────────────────────────────────────
|
||||
|
||||
/** The KSP epoch: year 1, day 1, 00:00:00 UT, in seconds. */
|
||||
export const KSP_EPOCH_UT = 0;
|
||||
|
||||
/** Default polling cadence for clients that don't open a WebSocket. */
|
||||
export const DEFAULT_SNAPSHOT_TTL_SECONDS = 30;
|
||||
@@ -0,0 +1,111 @@
|
||||
/**
|
||||
* Zod schemas for runtime validation of API payloads. These mirror the
|
||||
* TypeScript types in `index.ts` and are re-exported so the API can
|
||||
* validate ingest payloads and the frontends can validate responses
|
||||
* if they want stronger guarantees.
|
||||
*/
|
||||
import { z } from 'zod';
|
||||
|
||||
export const KeplerianElementsSchema = z.object({
|
||||
semiMajorAxis: z.number().nonnegative(),
|
||||
eccentricity: z.number().min(0),
|
||||
inclination: z.number(),
|
||||
longitudeOfAscendingNode: z.number(),
|
||||
argumentOfPeriapsis: z.number(),
|
||||
meanAnomalyAtEpoch: z.number(),
|
||||
epoch: z.number(),
|
||||
});
|
||||
|
||||
export const CelestialBodySchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
kind: z.enum(['star', 'planet', 'moon', 'asteroid', 'comet']),
|
||||
parentId: z.string().nullable(),
|
||||
radius: z.number().nonnegative(),
|
||||
sphereOfInfluence: z.number().nonnegative(),
|
||||
gravitationalParameter: z.number().nonnegative(),
|
||||
rotationPeriod: z.number().nonnegative(),
|
||||
axialTilt: z.number(),
|
||||
orbit: KeplerianElementsSchema,
|
||||
});
|
||||
|
||||
export const VesselSituationSchema = z.enum([
|
||||
'ORBITING',
|
||||
'ESCAPING',
|
||||
'LANDED',
|
||||
'SPLASHED',
|
||||
'PRELAUNCH',
|
||||
'FLYING',
|
||||
'SUB_ORBITAL',
|
||||
'DOCKED',
|
||||
'UNKNOWN',
|
||||
]);
|
||||
|
||||
export const VesselStatusSchema = z.enum(['ACTIVE', 'DECOMMISSIONED', 'LOST']);
|
||||
|
||||
export const VesselSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
type: z.string().nullable(),
|
||||
owner: z.string().nullable(),
|
||||
situation: VesselSituationSchema,
|
||||
status: VesselStatusSchema,
|
||||
orbit: KeplerianElementsSchema,
|
||||
referenceBodyId: z.string(),
|
||||
createdAt: z.string(),
|
||||
retiredAt: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const GroundStationSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
bodyId: z.string(),
|
||||
lat: z.number(),
|
||||
lon: z.number(),
|
||||
alt: z.number(),
|
||||
});
|
||||
|
||||
export const UniverseSnapshotSchema = z.object({
|
||||
ut: z.number(),
|
||||
capturedAt: z.string(),
|
||||
activeVesselId: z.string().nullable(),
|
||||
bodies: z.array(CelestialBodySchema),
|
||||
vessels: z.array(VesselSchema),
|
||||
groundStations: z.array(GroundStationSchema),
|
||||
});
|
||||
|
||||
export const MissionEventSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
mission: z.string(),
|
||||
vesselId: z.string().nullable(),
|
||||
title: z.string(),
|
||||
description: z.string().nullable(),
|
||||
scheduledAt: z.string(),
|
||||
utAtEvent: z.number().nullable(),
|
||||
durationSeconds: z.number().int().nullable(),
|
||||
kind: z.enum([
|
||||
'LAUNCH',
|
||||
'BURN',
|
||||
'FLYBY',
|
||||
'LANDING',
|
||||
'DOCKING',
|
||||
'EVA',
|
||||
'ECLIPSE',
|
||||
'OPERATION',
|
||||
'OTHER',
|
||||
]),
|
||||
createdBy: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
|
||||
export const MediaItemSchema = z.object({
|
||||
id: z.number().int().positive(),
|
||||
kind: z.enum(['image', 'youtube']),
|
||||
url: z.string().url(),
|
||||
thumbnailUrl: z.string().url().nullable(),
|
||||
caption: z.string().nullable(),
|
||||
tags: z.array(z.string()),
|
||||
missionId: z.string().nullable(),
|
||||
vesselId: z.string().nullable(),
|
||||
createdAt: z.string(),
|
||||
});
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"]
|
||||
}
|
||||
Reference in New Issue
Block a user