From 75f5371b747e73ebbbe4f87f04730e88f234a8b5 Mon Sep 17 00:00:00 2001 From: Andrew Munro Date: Sun, 21 Jan 2024 21:28:04 +0000 Subject: [PATCH] Moving server to bun to fix instability issues --- .github/workflows/build-and-push.yml | 101 +++++++------ .gitignore | 3 +- Dockerfile | 7 +- bun.lockb | Bin 0 -> 74595 bytes package.json | 21 ++- src/server.ts | 214 ++++++++++++++------------- src/utils.ts | 83 +++++------ src/views/transportScreen.ts | 50 +++---- vite.config.ts | 13 +- 9 files changed, 253 insertions(+), 239 deletions(-) create mode 100755 bun.lockb diff --git a/.github/workflows/build-and-push.yml b/.github/workflows/build-and-push.yml index 5238686..c90ca05 100644 --- a/.github/workflows/build-and-push.yml +++ b/.github/workflows/build-and-push.yml @@ -1,50 +1,63 @@ name: Build and Push Docker Image on: - push: - branches: [main] + push: + branches: [main] env: - TAG: latest + TAG: latest jobs: - build-and-push: - runs-on: ubuntu-latest - - steps: - - name: Set up QEMU - uses: docker/setup-qemu-action@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v2 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push Docker image - uses: docker/build-push-action@v4 - with: - push: true - tags: ghcr.io/${{ github.repository }}:${{ env.TAG }} - - - name: deploy-dev - uses: th0th/rancher-redeploy-workload@v0.9 - env: - RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} - RANCHER_URL: 'https://rancher.mun.sh' - RANCHER_CLUSTER_ID: 'local' - RANCHER_PROJECT_ID: 'p-vdwsg' - RANCHER_NAMESPACE: 'default' - RANCHER_WORKLOADS: 'rgb' - - - name: deploy-dev - uses: th0th/rancher-redeploy-workload@v0.9 - env: - RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} - RANCHER_URL: 'https://rancher.mun.sh' - RANCHER_CLUSTER_ID: 'local' - RANCHER_PROJECT_ID: 'p-vdwsg' - RANCHER_NAMESPACE: 'default' - RANCHER_WORKLOADS: 'rgb-browser' \ No newline at end of file + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v2 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install deps + run: bun install + + - name: Build + run: bun run build + + - name: Build and push Docker image + uses: docker/build-push-action@v4 + with: + push: true + tags: ghcr.io/${{ github.repository }}:${{ env.TAG }} + + - name: deploy-dev + uses: th0th/rancher-redeploy-workload@v0.9 + env: + RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} + RANCHER_URL: 'https://rancher.mun.sh' + RANCHER_CLUSTER_ID: 'local' + RANCHER_PROJECT_ID: 'p-vdwsg' + RANCHER_NAMESPACE: 'default' + RANCHER_WORKLOADS: 'rgb' + + - name: deploy-dev + uses: th0th/rancher-redeploy-workload@v0.9 + env: + RANCHER_BEARER_TOKEN: ${{ secrets.RANCHER_BEARER_TOKEN }} + RANCHER_URL: 'https://rancher.mun.sh' + RANCHER_CLUSTER_ID: 'local' + RANCHER_PROJECT_ID: 'p-vdwsg' + RANCHER_NAMESPACE: 'default' + RANCHER_WORKLOADS: 'rgb-browser' diff --git a/.gitignore b/.gitignore index 3ec544c..b3eb616 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules/ -.env \ No newline at end of file +.env +dist/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index cc0063e..f1298ed 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,7 @@ -FROM node:16 +FROM oven/bun COPY . /app WORKDIR /app -RUN yarn - -CMD [ "yarn", "start" ] \ No newline at end of file +EXPOSE 3000 +CMD [ "bun", "src/server.ts" ] \ No newline at end of file diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000000000000000000000000000000000000..802148435bd37f7da14908985f0c9c6b1fee81ca GIT binary patch literal 74595 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index 2880289..7ac1901 100644 --- a/package.json +++ b/package.json @@ -5,24 +5,21 @@ "type": "module", "scripts": { "start": "vite --host --port 3000", + "serv": "bun --watch src/server.ts", + "build": "vite build", "headless": "docker run -d --rm -e TZ=Europe/London -p 9222:9222 zenika/alpine-chrome --no-sandbox --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 http://mun.sh:3000" }, "license": "MIT", "dependencies": { "@pixi/gif": "^2.1.0", - "@pixi/node": "^7.2.0", - "@types/express": "^4.17.17", - "@types/ws": "^8.5.4", - "discord.js": "^14.8.0", - "dotenv": "^16.0.3", + "bun-serve-express": "^1.0.4", "express": "^4.18.2", - "express-ws": "^5.0.2", "gsap": "^3.11.5", - "node-fetch": "^3.3.1", "pixi.js-legacy": "^7.2.1", - "tsx": "^3.12.5", - "vite": "^4.2.0", - "vite-plugin-mix": "^0.4.0", - "ws": "^8.13.0" + "vite": "^5.0.12" + }, + "devDependencies": { + "@types/bun": "^1.0.3", + "@types/express": "^4.17.21" } -} +} \ No newline at end of file diff --git a/src/server.ts b/src/server.ts index 368f884..2bffd69 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,73 +1,88 @@ +import { ServerWebSocket, WebSocketServeOptions } from 'bun'; +import bunExpress from 'bun-serve-express'; import express from 'express'; -import { OPEN, WebSocket, WebSocketServer } from 'ws'; -import { webSocketServer } from './ws/websocketServer'; -import * as dotenv from 'dotenv'; -import fetch from 'node-fetch'; -// import { Client as DiscordClient, Events, GatewayIntentBits, SlashCommandBuilder } from 'discord.js'; -dotenv.config(); +// import { webSocketServer } from './ws/websocketServer'; process.env.TZ = 'Europe/London'; -const app = express(); -const wssPub = webSocketServer(app, '/pub'); -const wssSub = webSocketServer(app, '/sub'); +type WebSocketData = { + url: URL; + id: string; +}; + +const randomId = () => { + return Math.random().toString(36).substring(2, 7); +}; + +const app = bunExpress({ + websocket: { + open(ws) { + ws.data.id = randomId(); + + if (ws.data.url.pathname == '/pub') { + pubSockets.push(ws); + console.log('new publisher', ws.data.id); + } + + if (ws.data.url.pathname == '/sub') { + subSockets.push(ws); + console.log('new subscriber', ws.data.id); + } + }, + message(ws, message) { + if (ws.data.url.pathname == '/pub') { + // Ensure is binary data + if (typeof message == 'string') return; + + // TODO figure out better solution + // Ensure only first connected WS data is forwarded to RGB screen + if (ws.data.id != pubSockets[0].data.id) return; + + // Echo data back to sub sockets + for (const sub of subSockets) { + sub.send(message); + } + } + + if (ws.data.url.pathname == '/sub') { + console.log('message from sub', message); + } + }, + close(ws, code, message) { + if (ws.data.url.pathname == '/pub') { + pubSockets.splice(pubSockets.indexOf(ws), 1); + console.log('publisher disconnected'); + } + + if (ws.data.url.pathname == '/sub') { + subSockets.splice(subSockets.indexOf(ws), 1); + console.log('subscriber disconnected'); + } + }, + drain(ws) {} + } +} as WebSocketServeOptions as any); + const RTTAuth = Buffer.from(`${process.env.RTT_USER}:${process.env.RTT_PASS}`).toString('base64'); const FirstBusAuth = process.env.FIRST_BUS_API_KEY; -const pubSockets: WebSocket[] = []; -const subSockets: WebSocket[] = []; - -wssPub.on('connection', (socket, req) => { - pubSockets.push(socket); - console.log('new publisher'); - - socket.on('message', (data, isBinary) => { - // Ensure is binary data - if (!isBinary) return; - - // TODO figure out better solution - // Ensure only first connected WS data is forwarded to RGB screen - if (socket != pubSockets[0]) return; - - // Echo data back to sub sockets - for (const sub of subSockets) { - sub.send(data); - } - }); - - socket.once('close', () => { - pubSockets.splice(pubSockets.indexOf(socket), 1); - console.log('publisher disconnected'); - }); -}); - -wssSub.on('connection', (socket, req) => { - subSockets.push(socket); - console.log('new subscriber'); +const pubSockets: ServerWebSocket[] = []; +const subSockets: ServerWebSocket[] = []; - socket.on('message', (data, isBinary) => { - // Ensure is binary data - console.log("message from sub", data); - }); - - socket.once('close', () => { - subSockets.splice(subSockets.indexOf(socket), 1); - console.log('subscriber disconnected'); - }); -}); +app.use(express.static('dist')); app.get('/api/train/:station', async (req, res) => { const response = await fetch(`https://api.rtt.io/api/v1/json/search/${req.params.station}`, { headers: { - 'Authorization': `Basic ${RTTAuth}`, + Authorization: `Basic ${RTTAuth}` } }); - let data = await response.json() as any; + let data = (await response.json()) as any; - // console.log(data); + const formatDate = (dateString: string) => { + if (!dateString) return null; - const formatDate = (dateString:string) => { const split = dateString.split(''); const hours = split[0] + split[1]; const mins = split[2] + split[3]; @@ -81,15 +96,15 @@ app.get('/api/train/:station', async (req, res) => { time: `${hours}:${mins}`, date: d }; - } - + }; + // https://www.realtimetrains.co.uk/about/developer/pull/docs/locationlist/ - let formatted = data.services.map((train:any) => ({ + let formatted = data.services.map((train: any) => ({ scheduled: formatDate(train.locationDetail.gbttBookedArrival), - arrives: formatDate(train.locationDetail.realtimeArrival), + arrives: formatDate(train.locationDetail.realtimeArrival) ?? formatDate(train.locationDetail.gbttBookedArrival), origin: train.locationDetail.origin[0].description, destination: train.locationDetail.destination[0].description, - platform: train.locationDetail.platform ?? train.locationDetail.destination[0].description == 'Leeds' ? "1" : "2", + platform: train.locationDetail.platform ?? train.locationDetail.destination[0].description == 'Leeds' ? '1' : '2', displayAs: train.locationDetail.displayAs // CALL, PASS, ORIGIN, DESTINATION, STARTS, TERMINATES, CANCELLED_CALL, CANCELLED_PASS })); @@ -101,82 +116,77 @@ const getBusStopIds = (stopName: string) => { if (stopName == 'kirkstall_lane') return ['450011444', '450011458']; throw new Error('stop not found: ' + stopName); -} +}; const getBusesByStopId = async (stop: string) => { const response = await fetch(`https://prod.mobileapi.firstbus.co.uk/api/v2/bus/stop/${stop}/departure`, { headers: { - 'x-app-key': `${FirstBusAuth}`, + 'x-app-key': `${FirstBusAuth}` } }); - let data = await response.json() as any; + let data = (await response.json()) as any; return data.data.attributes['live-departures'].concat(data.data.attributes['timetable-departures']).map(departure => ({ line: departure.line, scheduled: { - time: new Date(departure['scheduled-time'] || departure['departure-time']).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }), - date: new Date(departure['scheduled-time'] || departure['departure-time']), + time: new Date(departure['scheduled-time'] || departure['departure-time']).toLocaleTimeString('en-GB', { + hour: '2-digit', + minute: '2-digit' + }), + date: new Date(departure['scheduled-time'] || departure['departure-time']) }, expectedArrivalMins: departure['is-live'] ? Math.floor(departure['expected-time-in-seconds'] / 60) : null, occupancy: departure['occupancy'] ? departure['occupancy']['types'][0] : null })); -} +}; app.get('/api/bus/:stop', async (req, res) => { const stops = getBusStopIds(req.params.stop); const busses = (await Promise.all(stops?.map(sid => getBusesByStopId(sid)))).flat(); - const getArrivalOrScheduledTime = (bus) => { + const getArrivalOrScheduledTime = bus => { if (bus.expectedArrivalMins != null) { - return new Date(Date.now() + 1000 * 60 * bus.expectedArrivalMins) + return new Date(Date.now() + 1000 * 60 * bus.expectedArrivalMins); } return bus.scheduled.date; - } - + }; + res.json(busses.sort((a, b) => getArrivalOrScheduledTime(a) - getArrivalOrScheduledTime(b))); }); app.get('/api/flights/arrivals', async (req, res) => { const response = await fetch(`https://lba-flights.production.parallax.dev/arrivals`); - const data = await response.json() as any; - - const flights = data.map(fl => ({ - id: fl.flight_ident, - scheduled: { - time: new Date(fl.scheduled_time).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }), - date: new Date(fl.scheduled_time), - }, - origin: fl.airport_name, - message: fl.message ? fl.message.replace('Expected', 'exp').replace('Landed', 'lnd').replace('Now ', '') : null, - status: fl.status, - })).filter(fl => { - if (fl.status != 'LND') return true; - - const timeString = fl.message.split(" ")[1]; - const date = new Date(); - date.setHours(timeString.split(":")[0]); - date.setMinutes(timeString.split(":")[1]); - - return new Date().getTime() - date.getTime() <= 1000 * 60 * 5; - }); + const data = (await response.json()) as any; + + const flights = data + .map((fl: any) => ({ + id: fl.flight_ident, + scheduled: { + time: new Date(fl.scheduled_time).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' }), + date: new Date(fl.scheduled_time) + }, + origin: fl.airport_name, + message: fl.message ? fl.message.replace('Expected', 'exp').replace('Landed', 'lnd').replace('Now ', '') : null, + status: fl.status + })) + .filter((fl: any) => { + if (fl.status != 'LND') return true; + + const timeString = fl.message.split(' ')[1]; + const date = new Date(); + date.setHours(timeString.split(':')[0]); + date.setMinutes(timeString.split(':')[1]); + + return new Date().getTime() - date.getTime() <= 1000 * 60 * 5; + }); res.json(flights); }); -// const client = new DiscordClient({ intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds] }); -// client.on('message', msg => { -// if (msg.content === 'ping') { -// msg.reply('Pong!'); -// } -// }); - -// client.once(Events.ClientReady, c => { -// console.log(`Ready! Logged in as ${c.user.tag}`); -// }); - -// client.login(process.env.DISCORD_CLIENT_TOKEN); -export const handler = app; +app.listen(3000, () => { + console.log('server started on', 3000); +}); diff --git a/src/utils.ts b/src/utils.ts index ea29da1..4a73324 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,29 +1,29 @@ import { Assets, BitmapText, Filter, IBitmapTextStyle } from 'pixi.js-legacy'; export function rgbaToRgb565(rgba: Uint8Array) { - const length = rgba.length / 4; - const rgb565 = new Uint16Array(length); + const length = rgba.length / 4; + const rgb565 = new Uint16Array(length); - for (let i = 0; i < length; i++) { - const r = rgba[i * 4]; - const g = rgba[i * 4 + 1]; - const b = rgba[i * 4 + 2]; + for (let i = 0; i < length; i++) { + const r = rgba[i * 4]; + const g = rgba[i * 4 + 1]; + const b = rgba[i * 4 + 2]; - const r5 = ((r * 31) / 255) | 0; - const g6 = ((g * 63) / 255) | 0; - const b5 = ((b * 31) / 255) | 0; + const r5 = ((r * 31) / 255) | 0; + const g6 = ((g * 63) / 255) | 0; + const b5 = ((b * 31) / 255) | 0; - const rgb = (r5 << 11) | (g6 << 5) | b5; - rgb565[i] = rgb; - } + const rgb = (r5 << 11) | (g6 << 5) | b5; + rgb565[i] = rgb; + } - // Convert Uint16Array to array of hex byte strings - const hexStrings = new Array(rgb565.length); - for (let i = 0; i < rgb565.length; i++) { - hexStrings[i] = '0x' + (rgb565[i] >> 8).toString(16).padStart(2, '0') + (rgb565[i] & 0xff).toString(16).padStart(2, '0'); - } + // Convert Uint16Array to array of hex byte strings + const hexStrings = new Array(rgb565.length); + for (let i = 0; i < rgb565.length; i++) { + hexStrings[i] = '0x' + (rgb565[i] >> 8).toString(16).padStart(2, '0') + (rgb565[i] & 0xff).toString(16).padStart(2, '0'); + } - return rgb565; + return rgb565; } const dotMatrixShader = ` @@ -58,32 +58,31 @@ void main() { // Create the PixiJS filter with the dot matrix shader export const dotMatrixFilter = (width: number, height: number) => - new Filter(undefined, dotMatrixShader, { - uResolution: [width, height], - dotSize: 0.35, // Adjust this value to change the dot size (0.0 - 1.0) - cellSize: 10 // Adjust this value to change the cell size (in pixels) - }); + new Filter(undefined, dotMatrixShader, { + uResolution: [width, height], + dotSize: 0.35, // Adjust this value to change the dot size (0.0 - 1.0) + cellSize: 10 // Adjust this value to change the cell size (in pixels) + }); - -await Assets.load('silkscreen.fnt'); -await Assets.load('pixel7.fnt'); +await Assets.load('https://cdn.mun.sh/silkscreen.fnt'); +await Assets.load('https://cdn.mun.sh/pixel7.fnt'); const textStyle: Partial = { - fontName: 'silkscreen', - fontSize: 8, - tint: 'white', - align: 'left', - letterSpacing: -1 + fontName: 'silkscreen', + fontSize: 8, + tint: 'white', + align: 'left', + letterSpacing: -1 }; -export const createText = (text: string, tint = 'yellow', font: 'silkscreen'|'pixel7' = 'silkscreen') => { - text = text.replaceAll(':', ' :'); - - return new BitmapText(text, { - ...textStyle, - tint, - fontName: font, - fontSize: font == 'silkscreen' ? 8 : 10, - letterSpacing: font == 'silkscreen' ? -1 : 0 - }); -}; \ No newline at end of file +export const createText = (text: string, tint = 'yellow', font: 'silkscreen' | 'pixel7' = 'silkscreen') => { + text = text.replaceAll(':', ' :'); + + return new BitmapText(text, { + ...textStyle, + tint, + fontName: font, + fontSize: font == 'silkscreen' ? 8 : 10, + letterSpacing: font == 'silkscreen' ? -1 : 0 + }); +}; diff --git a/src/views/transportScreen.ts b/src/views/transportScreen.ts index 57e3fde..36e4a5d 100644 --- a/src/views/transportScreen.ts +++ b/src/views/transportScreen.ts @@ -1,37 +1,37 @@ -import { Container } from "pixi.js-legacy"; -import { buses } from "./buses"; -import { flights } from "./flights"; -import { trains } from "./trains"; +import { Container } from 'pixi.js-legacy'; +import { buses } from './buses'; +import { flights } from './flights'; +import { trains } from './trains'; const interval = 1000 * 10; const getScreen = async function* (parent: Container) { - while (true) { - yield await buses(parent, 'kirkstall_lights'); - yield await buses(parent, 'kirkstall_lane'); - yield await trains(parent); - yield await flights(parent); - } -} + while (true) { + yield await buses(parent, 'kirkstall_lane'); + yield await buses(parent, 'kirkstall_lights'); + yield await trains(parent); + yield await flights(parent); + } +}; export const transportScreen = () => { - const display = new Container(); + const display = new Container(); - const it = getScreen(display); - let current: any = null; + const it = getScreen(display); + let current: any = null; - const nextScreen = async () => { - const next = await it.next(); + const nextScreen = async () => { + const next = await it.next(); - if (current) { - current.destroy(); - } + if (current) { + current.destroy(); + } - current = next.value; - } + current = next.value; + }; - nextScreen(); - setInterval(() => nextScreen(), interval) + nextScreen(); + setInterval(() => nextScreen(), interval); - return display; -} + return display; +}; diff --git a/vite.config.ts b/vite.config.ts index 4474be2..1061fe1 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,13 +1,8 @@ import { defineConfig } from 'vite'; -import mix from 'vite-plugin-mix'; export default defineConfig({ - server: { - hmr: !process.env.DISABLE_HMR, - }, - plugins: [ - mix.default({ - handler: './src/server.ts' - }) - ] + build: { + target: 'esnext', + minify: false + } });