179 lines
4.0 KiB
JavaScript
179 lines
4.0 KiB
JavaScript
import 'dotenv/config';
|
|
|
|
import { watch } from 'node:fs';
|
|
import { createServer } from 'node:http';
|
|
|
|
import { Server } from 'socket.io';
|
|
|
|
import contentStore from './contentStore.cjs';
|
|
|
|
const defaultWebOrigin = 'http://127.0.0.1:5173';
|
|
const host = process.env.TOOLKIT_SOCKET_HOST ?? '127.0.0.1';
|
|
const port = Number.parseInt(process.env.TOOLKIT_SOCKET_PORT ?? '5174', 10);
|
|
const allowedOrigins = (
|
|
process.env.TOOLKIT_SOCKET_CORS_ORIGIN ?? defaultWebOrigin
|
|
)
|
|
.split(',')
|
|
.map(origin => origin.trim())
|
|
.filter(Boolean);
|
|
|
|
if (Number.isNaN(port)) {
|
|
throw new Error('TOOLKIT_SOCKET_PORT must be a valid number.');
|
|
}
|
|
|
|
const httpServer = createServer((request, response) => {
|
|
if (request.url === '/health') {
|
|
response.writeHead(200, {
|
|
'Content-Type': 'application/json',
|
|
});
|
|
response.end(
|
|
JSON.stringify({
|
|
ok: true,
|
|
service: 'blb-toolkit-socket',
|
|
}),
|
|
);
|
|
return;
|
|
}
|
|
|
|
response.writeHead(404, {
|
|
'Content-Type': 'application/json',
|
|
});
|
|
response.end(
|
|
JSON.stringify({
|
|
ok: false,
|
|
service: 'blb-toolkit-socket',
|
|
}),
|
|
);
|
|
});
|
|
|
|
const io = new Server(httpServer, {
|
|
cors: {
|
|
methods: ['GET', 'POST'],
|
|
origin: allowedOrigins,
|
|
},
|
|
});
|
|
|
|
const contentWatchDebounceMs = 100;
|
|
let contentWatchTimer = null;
|
|
|
|
function getErrorMessage(error) {
|
|
return error instanceof Error ? error.message : 'Unknown toolkit content error.';
|
|
}
|
|
|
|
function createContentError(resource, error) {
|
|
return {
|
|
error: getErrorMessage(error),
|
|
resource,
|
|
};
|
|
}
|
|
|
|
async function readContent(resource) {
|
|
return contentStore.readJsonResource(resource);
|
|
}
|
|
|
|
async function saveContent(resource, content) {
|
|
return contentStore.writeJsonResource(resource, content);
|
|
}
|
|
|
|
async function broadcastChangedContent(resource) {
|
|
try {
|
|
io.emit('toolkit:content:changed', await readContent(resource));
|
|
} catch (error) {
|
|
io.emit('toolkit:content:error', createContentError(resource, error));
|
|
}
|
|
}
|
|
|
|
function scheduleContentBroadcast(resource) {
|
|
if (contentWatchTimer != null) {
|
|
clearTimeout(contentWatchTimer);
|
|
}
|
|
|
|
contentWatchTimer = setTimeout(() => {
|
|
contentWatchTimer = null;
|
|
broadcastChangedContent(resource);
|
|
}, contentWatchDebounceMs);
|
|
}
|
|
|
|
const creditsWatcher = watch(
|
|
contentStore.defaultResourcePaths.credits,
|
|
{ persistent: false },
|
|
() => {
|
|
scheduleContentBroadcast('credits');
|
|
},
|
|
);
|
|
|
|
io.on('connection', socket => {
|
|
socket.emit('toolkit:ready', {
|
|
connectedAt: new Date().toISOString(),
|
|
socketId: socket.id,
|
|
});
|
|
|
|
socket.on('toolkit:event', payload => {
|
|
socket.broadcast.emit('toolkit:event', payload);
|
|
});
|
|
|
|
socket.on('toolkit:content:read', async (payload, respond) => {
|
|
const resource = payload?.resource;
|
|
|
|
try {
|
|
respond?.({
|
|
ok: true,
|
|
...(await readContent(resource)),
|
|
});
|
|
} catch (error) {
|
|
const contentError = createContentError(resource, error);
|
|
|
|
respond?.({
|
|
ok: false,
|
|
...contentError,
|
|
});
|
|
socket.emit('toolkit:content:error', contentError);
|
|
}
|
|
});
|
|
|
|
socket.on('toolkit:content:save', async (payload, respond) => {
|
|
const resource = payload?.resource;
|
|
|
|
try {
|
|
const changedContent = await saveContent(resource, payload?.content);
|
|
|
|
respond?.({
|
|
ok: true,
|
|
...changedContent,
|
|
});
|
|
io.emit('toolkit:content:changed', changedContent);
|
|
} catch (error) {
|
|
const contentError = createContentError(resource, error);
|
|
|
|
respond?.({
|
|
ok: false,
|
|
...contentError,
|
|
});
|
|
socket.emit('toolkit:content:error', contentError);
|
|
}
|
|
});
|
|
});
|
|
|
|
function closeServer(signal) {
|
|
console.log(`[toolkit-socket] received ${signal}; shutting down`);
|
|
|
|
creditsWatcher.close();
|
|
|
|
io.close(() => {
|
|
httpServer.close(() => {
|
|
process.exit(0);
|
|
});
|
|
});
|
|
}
|
|
|
|
process.on('SIGINT', () => closeServer('SIGINT'));
|
|
process.on('SIGTERM', () => closeServer('SIGTERM'));
|
|
|
|
httpServer.listen(port, host, () => {
|
|
console.log(
|
|
`[toolkit-socket] listening at http://${host}:${port} with CORS ${allowedOrigins.join(
|
|
', ',
|
|
)}`,
|
|
);
|
|
});
|