Configure board deployment and backend proxy
Build, Push and Deploy / build (push) Failing after 1s
Build, Push and Deploy / deploy (push) Skipped

This commit is contained in:
2026-09-13 16:09:59 +08:00
commit 02f5bf14ba
91 changed files with 12401 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules
.DS_Store
+13
View File
@@ -0,0 +1,13 @@
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY realtime-server.mjs ./
USER node
EXPOSE 8787
HEALTHCHECK --interval=15s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://127.0.0.1:8787/healthz | grep -q '"status":"ok"'
CMD ["node", "realtime-server.mjs"]
+36
View File
@@ -0,0 +1,36 @@
{
"name": "wc-board-realtime",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "wc-board-realtime",
"version": "0.1.0",
"dependencies": {
"ws": "^8.18.3"
}
},
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "wc-board-realtime",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "node realtime-server.mjs",
"start": "node realtime-server.mjs",
"test": "node --test"
},
"dependencies": {
"ws": "^8.18.3"
}
}
+91
View File
@@ -0,0 +1,91 @@
import { createServer } from 'node:http';
import { pathToFileURL } from 'node:url';
import { WebSocketServer } from 'ws';
function isFocusEvent(value) {
return (
typeof value === 'object' &&
value !== null &&
value.type === 'FOCUS_NAME' &&
typeof value.cloudId === 'string' &&
typeof value.name === 'string' &&
typeof value.messageId === 'string' &&
typeof value.sentAt === 'number' &&
(value.targetIndex === undefined ||
(typeof value.targetIndex === 'number' &&
Number.isInteger(value.targetIndex) &&
value.targetIndex >= 0))
);
}
export function createRealtimeServer(port = Number(process.env.REALTIME_PORT || 8787)) {
const server = createServer((request, response) => {
if (request.url === '/healthz') {
response.writeHead(200, { 'content-type': 'application/json' });
response.end(JSON.stringify({ status: 'ok' }));
return;
}
response.writeHead(404, { 'content-type': 'text/plain' });
response.end('Not found');
});
const realtime = new WebSocketServer({
server,
maxPayload: 16 * 1024,
});
const heartbeat = setInterval(() => {
for (const socket of realtime.clients) {
if (socket.isAlive === false) {
socket.terminate();
continue;
}
socket.isAlive = false;
socket.ping();
}
}, 30_000);
heartbeat.unref();
realtime.on('connection', (socket) => {
socket.isAlive = true;
socket.on('pong', () => {
socket.isAlive = true;
});
socket.on('message', (data) => {
let value;
try {
value = JSON.parse(data.toString());
} catch {
return;
}
if (!isFocusEvent(value)) {
return;
}
const message = JSON.stringify(value);
for (const client of realtime.clients) {
if (client !== socket && client.readyState === client.OPEN) {
client.send(message);
}
}
});
});
realtime.on('close', () => {
clearInterval(heartbeat);
});
return server;
}
if (import.meta.url === pathToFileURL(process.argv[1] || '').href) {
const port = Number(process.env.REALTIME_PORT || 8787);
createRealtimeServer(port).listen(port, () => {
console.log(`Realtime WebSocket server listening on port ${port}`);
});
}
+45
View File
@@ -0,0 +1,45 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { createRealtimeServer } from './realtime-server.mjs';
import WebSocket from 'ws';
test('broadcasts focus events to other connected clients', async () => {
const server = createRealtimeServer(0);
await new Promise((resolve) => server.listen(resolve));
const { port } = server.address();
try {
const screen = new WebSocket(`ws://127.0.0.1:${port}`);
const controller = new WebSocket(`ws://127.0.0.1:${port}`);
await Promise.all([
new Promise((resolve) => screen.once('open', resolve)),
new Promise((resolve) => controller.once('open', resolve)),
]);
const received = new Promise((resolve) => {
screen.once('message', (data) => resolve(JSON.parse(data)));
});
controller.send(JSON.stringify({
type: 'FOCUS_NAME',
cloudId: 'demo',
name: 'Sophia',
messageId: 'focus-1',
sentAt: 1,
}));
assert.deepEqual(await received, {
type: 'FOCUS_NAME',
cloudId: 'demo',
name: 'Sophia',
messageId: 'focus-1',
sentAt: 1,
});
screen.close();
controller.close();
} finally {
await new Promise((resolve) => server.close(resolve));
}
});