Add live reload on changes
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"express-rate-limit": "^7.5.0",
|
||||
"pino": "^9.6.0",
|
||||
"pino-pretty": "^13.0.0",
|
||||
"ws": "^8.20.0",
|
||||
"zod": "^3.24.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -26,6 +27,7 @@
|
||||
"@types/cors": "^2.8.17",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/jsonwebtoken": "^9.0.7",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tsx": "^4.19.0",
|
||||
"typescript": "^5.8.0"
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createEditQueue } from './queue/edit-queue.js';
|
||||
import { processEditJob } from './queue/process-edit-job.js';
|
||||
import { openDb, closeDb, pruneExpiredProposals, pruneIdempotencyKeys } from './db.js';
|
||||
import { logger } from './logger.js';
|
||||
import { initLiveReload } from './live-reload.js';
|
||||
|
||||
const PORT = parseInt(process.env.ORCHESTRATOR_PORT || '3001', 10);
|
||||
|
||||
@@ -27,6 +28,9 @@ export async function startServer() {
|
||||
logger.info({ event: 'server.started', port: PORT }, `Orchestrator listening on port ${PORT}`);
|
||||
});
|
||||
|
||||
// Optional websocket for browser reload on content writes
|
||||
initLiveReload(server);
|
||||
|
||||
// Graceful shutdown
|
||||
let shuttingDown = false;
|
||||
async function shutdown(signal: string) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import crypto from 'node:crypto';
|
||||
import { stringifyCanonical } from '@dynamic-sites/shared';
|
||||
import { writeAuditLog } from '../db.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { broadcastReload } from '../live-reload.js';
|
||||
|
||||
const REPO_ROOT = process.env.REPO_ROOT || '.';
|
||||
const MAX_BACKUPS = 20;
|
||||
@@ -70,4 +71,13 @@ export function writeContentFile(
|
||||
});
|
||||
|
||||
logger.info({ event: 'content.written', path: repoRelativePath, size: canonical.length }, 'Content file written');
|
||||
|
||||
// Notify any connected browsers to reload.
|
||||
broadcastReload('content.written', {
|
||||
path: repoRelativePath,
|
||||
proposalId: opts?.proposalId,
|
||||
source: opts?.source,
|
||||
beforeHash,
|
||||
afterHash,
|
||||
});
|
||||
}
|
||||
|
||||
45
server/src/live-reload.ts
Normal file
45
server/src/live-reload.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type http from 'node:http';
|
||||
import { WebSocketServer, type WebSocket } from 'ws';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
let wss: WebSocketServer | null = null;
|
||||
|
||||
export function initLiveReload(server: http.Server): void {
|
||||
if (wss) return;
|
||||
|
||||
const enabled = process.env.LIVE_RELOAD_WS_ENABLED === 'true';
|
||||
if (!enabled) {
|
||||
logger.info({ event: 'live_reload.disabled' }, 'Live reload websocket disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
const wsPath = process.env.LIVE_RELOAD_WS_PATH || '/__live_reload';
|
||||
|
||||
wss = new WebSocketServer({ server, path: wsPath });
|
||||
wss.on('connection', (socket: WebSocket) => {
|
||||
logger.info({ event: 'live_reload.connected' }, 'Live reload client connected');
|
||||
socket.on('close', () => {
|
||||
logger.info({ event: 'live_reload.disconnected' }, 'Live reload client disconnected');
|
||||
});
|
||||
});
|
||||
|
||||
logger.info({ event: 'live_reload.started', path: wsPath }, 'Live reload websocket started');
|
||||
}
|
||||
|
||||
export function broadcastReload(reason: string, data?: Record<string, unknown>): void {
|
||||
if (!wss) return;
|
||||
|
||||
const payload = JSON.stringify({
|
||||
type: 'reload',
|
||||
ts: Date.now(),
|
||||
reason,
|
||||
...data,
|
||||
});
|
||||
|
||||
for (const client of wss.clients) {
|
||||
if (client.readyState === client.OPEN) {
|
||||
client.send(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +145,12 @@ Example response:
|
||||
},
|
||||
];
|
||||
|
||||
return generateWithValidation({ messages, schema: routingOutputSchema, chat });
|
||||
const routed = await generateWithValidation({ messages, schema: routingOutputSchema, chat });
|
||||
// Some TS setups infer optional fields from the Zod schema; normalize to our contract type.
|
||||
return {
|
||||
...routed,
|
||||
needs_clarification: routed.needs_clarification ?? false,
|
||||
} satisfies RoutingOutput;
|
||||
}
|
||||
|
||||
// ── Message Classification ──
|
||||
|
||||
@@ -192,7 +192,8 @@ export function createApiEditRouter(deps: ApiEditRouterDeps): Router {
|
||||
|
||||
// GET /api/job/:id — poll for a proposal created by an async job
|
||||
router.get('/job/:id', (req: Request, res: Response) => {
|
||||
const entry = jobProposalMap.get(req.params.id);
|
||||
const jobId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
const entry = jobProposalMap.get(jobId);
|
||||
if (!entry) {
|
||||
res.json({ status: 'processing' });
|
||||
return;
|
||||
@@ -214,7 +215,8 @@ export function createApiEditRouter(deps: ApiEditRouterDeps): Router {
|
||||
|
||||
// GET /api/proposal/:id — check proposal status
|
||||
router.get('/proposal/:id', (req: Request, res: Response) => {
|
||||
const proposal = getProposal(req.params.id);
|
||||
const proposalId = Array.isArray(req.params.id) ? req.params.id[0] : req.params.id;
|
||||
const proposal = getProposal(proposalId);
|
||||
if (!proposal) {
|
||||
res.status(404).json({ error: 'Proposal not found' });
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user