83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import type { Plugin } from 'vite';
|
|
import { WebSocketServer } from 'ws';
|
|
import { handle, synthTelemetryHistory } from './handlers';
|
|
|
|
const COOKIE = 'mocksid';
|
|
const WS_PATH = /^\/api\/ws\/satellite\/([0-9a-f-]{36})\/telemetry\/$/i;
|
|
|
|
function readCookie(header: string | undefined, name: string): string | null {
|
|
if (!header) return null;
|
|
for (const part of header.split(';')) {
|
|
const [k, ...v] = part.trim().split('=');
|
|
if (k === name) return decodeURIComponent(v.join('='));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/** Dev-only middleware that answers /api/* from the in-memory mock backend. */
|
|
export function mockApi(): Plugin {
|
|
return {
|
|
name: 'mock-api',
|
|
configureServer(server) {
|
|
// Live telemetry: a real WebSocket so the tracking flow is exercised
|
|
// end-to-end (the read-only satellite consumer in the real backend).
|
|
const wss = new WebSocketServer({ noServer: true });
|
|
server.httpServer?.on('upgrade', (req, socket, head) => {
|
|
const url = req.url ?? '';
|
|
const match = url.split('?')[0].match(WS_PATH);
|
|
if (!match) return; // let Vite's own HMR socket handle it
|
|
wss.handleUpgrade(req, socket as never, head, (ws) => {
|
|
// Continue the satellite's synthetic track from where history ended.
|
|
const history = synthTelemetryHistory(match[1]);
|
|
let next = history.length;
|
|
const first = history[0] as { timestamp: number; lat: number; lon: number };
|
|
const timer = setInterval(() => {
|
|
ws.send(
|
|
JSON.stringify({
|
|
id: `${match[1]}-live-${next}`,
|
|
timestamp: first.timestamp + (next - history.length + 1) * 60,
|
|
lat: first.lat + (next - history.length + 1) * 0.004,
|
|
lon: first.lon + (next - history.length + 1) * 0.01,
|
|
alt: 18000 + (next - history.length + 1) * 900,
|
|
payload: { live: true },
|
|
raw_data: {}
|
|
})
|
|
);
|
|
next += 1;
|
|
}, 1000);
|
|
ws.on('close', () => clearInterval(timer));
|
|
});
|
|
});
|
|
|
|
server.middlewares.use((req, res, next) => {
|
|
if (!req.url?.startsWith('/api/')) return next();
|
|
let raw = '';
|
|
req.on('data', (c) => (raw += c));
|
|
req.on('end', () => {
|
|
const body = raw ? JSON.parse(raw) : {};
|
|
const sessionUser = readCookie(req.headers.cookie, COOKIE);
|
|
const [pathname, search = ''] = req.url!.split('?');
|
|
const result = handle(
|
|
req.method ?? 'GET',
|
|
pathname,
|
|
body,
|
|
sessionUser,
|
|
new URLSearchParams(search)
|
|
);
|
|
res.statusCode = result.status;
|
|
res.setHeader('content-type', 'application/json');
|
|
res.setHeader('x-csrftoken', 'mock-csrf');
|
|
if (result.session !== undefined) {
|
|
res.setHeader(
|
|
'set-cookie',
|
|
result.session === null
|
|
? `${COOKIE}=; Path=/; Max-Age=0; SameSite=Lax`
|
|
: `${COOKIE}=${encodeURIComponent(result.session)}; Path=/; SameSite=Lax`
|
|
);
|
|
}
|
|
res.end(JSON.stringify(result.body));
|
|
});
|
|
});
|
|
}
|
|
};
|
|
}
|