refactor: remove WebSocket implementation and switch to HTTP-only sync with configurable polling interval

This commit is contained in:
2025-10-26 15:12:45 +01:00
parent 74fecb3ec2
commit 394ebfd76c
11 changed files with 171 additions and 391 deletions
+3 -5
View File
@@ -19,10 +19,9 @@
"cors": "^2.8.5",
"better-sqlite3": "^11.0.0",
"dotenv": "^16.3.1",
"ws": "^8.16.0",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"eslint": "^8.57.0",
"@typescript-eslint/eslint-plugin": "^7.0.2",
"@typescript-eslint/parser": "^7.0.2",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"prettier": "^3.3.3",
"pg": "^8.11.3"
@@ -32,7 +31,6 @@
"@types/cors": "^2.8.17",
"@types/node": "^20.10.6",
"@types/better-sqlite3": "^7.6.11",
"@types/ws": "^8.5.10",
"@types/pg": "^8.10.2",
"typescript": "^5.3.3",
"ts-node-dev": "^2.0.0"
+2 -5
View File
@@ -8,8 +8,6 @@ import fieldRoutes from './routes/fieldRoutes';
import itemRoutes from './routes/itemRoutes';
import syncRoutes from './routes/syncRoutes';
import webRoutes from './routes/webRoutes';
import { createServer } from 'http';
import { initWebSocket } from './ws';
dotenv.config();
@@ -48,9 +46,8 @@ app.use('/', webRoutes);
console.log('- Items');
console.log('- ItemValues');
const server = createServer(app);
initWebSocket(server);
server.listen(PORT, () => {
// Start HTTP server (WebSocket removed)
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
console.log(`Environment: ${process.env.NODE_ENV || 'development'}`);
const dbClient = (process.env.DB_CLIENT || process.env.DB_TYPE || 'sqlite').toLowerCase();
-106
View File
@@ -1,106 +0,0 @@
import { Server as HttpServer, IncomingMessage } from 'http';
import { WebSocketServer, WebSocket } from 'ws';
import { dbAdapter } from './db';
interface SyncRequest {
lastSyncTimestamp: number;
lists: any[];
fields: any[];
items: any[];
itemValues: any[];
}
interface MessageEnvelope<T = any> {
id?: string;
type: string;
payload?: T;
}
function checkAuth(req: any): boolean {
const serverPassword = process.env.SERVER_PASSWORD;
if (!serverPassword) return true; // auth disabled
const auth = req.headers?.authorization as string | undefined;
if (!auth) return false;
const parts = auth.split(' ');
return parts.length === 2 && parts[0] === 'Bearer' && parts[1] === serverPassword;
}
const UPSERT_LIST = 'INSERT INTO lists (id, name, createdAt, updatedAt, isDeleted) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, updatedAt = EXCLUDED.updatedAt, isDeleted = EXCLUDED.isDeleted';
const UPSERT_FIELD = 'INSERT INTO fields (id, name, fieldType, fieldOptions, listId, "order", createdAt, updatedAt, isDeleted) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET name = EXCLUDED.name, fieldType = EXCLUDED.fieldType, fieldOptions = EXCLUDED.fieldOptions, "order" = EXCLUDED."order", updatedAt = EXCLUDED.updatedAt, isDeleted = EXCLUDED.isDeleted';
const UPSERT_ITEM = 'INSERT INTO items (id, listId, createdAt, updatedAt, isDeleted) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET updatedAt = EXCLUDED.updatedAt, isDeleted = EXCLUDED.isDeleted';
const UPSERT_ITEM_VALUE = 'INSERT INTO item_values (id, itemId, fieldId, value, updatedAt) VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET value = EXCLUDED.value, updatedAt = EXCLUDED.updatedAt';
async function processSync(data: SyncRequest) {
await dbAdapter.transaction(async (tx) => {
for (const list of data.lists || []) {
await tx.execute(UPSERT_LIST, [list.id, list.name, list.createdAt, list.updatedAt, list.isDeleted ? 1 : 0]);
}
for (const field of data.fields || []) {
await tx.execute(UPSERT_FIELD, [field.id, field.name, field.fieldType, field.fieldOptions, field.listId, field.order, field.createdAt, field.updatedAt, field.isDeleted ? 1 : 0]);
}
for (const item of data.items || []) {
await tx.execute(UPSERT_ITEM, [item.id, item.listId, item.createdAt, item.updatedAt, item.isDeleted ? 1 : 0]);
}
for (const val of data.itemValues || []) {
await tx.execute(UPSERT_ITEM_VALUE, [val.id, val.itemId, val.fieldId, val.value, val.updatedAt]);
}
});
let serverLists: any[], serverFields: any[], serverItems: any[], serverItemValues: any[];
const since = data.lastSyncTimestamp;
if (since === 0) {
serverLists = await dbAdapter.queryAll('SELECT * FROM lists WHERE isDeleted = 0');
serverFields = await dbAdapter.queryAll('SELECT * FROM fields WHERE isDeleted = 0');
serverItems = await dbAdapter.queryAll('SELECT * FROM items WHERE isDeleted = 0');
serverItemValues = await dbAdapter.queryAll('SELECT * FROM item_values');
} else {
serverLists = await dbAdapter.queryAll('SELECT * FROM lists WHERE updatedAt >= ?', [since]);
serverFields = await dbAdapter.queryAll('SELECT * FROM fields WHERE updatedAt >= ?', [since]);
serverItems = await dbAdapter.queryAll('SELECT * FROM items WHERE updatedAt >= ?', [since]);
serverItemValues = await dbAdapter.queryAll('SELECT * FROM item_values WHERE updatedAt >= ?', [since]);
}
serverLists = serverLists.map(l => ({ ...l, isDeleted: !!l.isDeleted }));
serverFields = serverFields.map(f => ({ ...f, isDeleted: !!f.isDeleted }));
serverItems = serverItems.map(i => ({ ...i, isDeleted: !!i.isDeleted }));
return {
lists: serverLists,
fields: serverFields,
items: serverItems,
itemValues: serverItemValues,
serverTimestamp: Date.now(),
};
}
export function initWebSocket(server: HttpServer) {
const wss = new WebSocketServer({ server, path: '/api/ws' });
console.log('WebSocket server initialized at /api/ws');
wss.on('connection', (ws: WebSocket, req: IncomingMessage) => {
if (!checkAuth(req)) {
ws.close(1008, 'Unauthorized');
return;
}
ws.on('message', async (raw: Buffer) => {
try {
const msg = JSON.parse(raw.toString()) as MessageEnvelope;
if (msg.type === 'ping') {
const resp: MessageEnvelope = { type: 'pong', id: msg.id };
ws.send(JSON.stringify(resp));
return;
}
if (msg.type === 'sync') {
const respData = await processSync(msg.payload as SyncRequest);
const resp: MessageEnvelope = { type: 'syncResponse', id: msg.id, payload: respData };
ws.send(JSON.stringify(resp));
return;
}
} catch (e) {
const err: MessageEnvelope = { type: 'error', payload: { message: 'Invalid message' } };
ws.send(JSON.stringify(err));
}
});
});
}