Created
May 12, 2023 03:09
-
-
Save rubys/a88964e20c7a24172d84ddcfc47bb76e to your computer and use it in GitHub Desktop.
Barebones demo of ejs, express, node, npm, redis, pg, tailwindcss, typescript, and ws
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# syntax = docker/dockerfile:1 | |
FROM node:18-slim as base | |
# Node app lives here | |
WORKDIR /app | |
COPY <<-"EOF" package.json | |
{ | |
"dependencies": { | |
"ejs": "^3.1.9", | |
"express": "^4.18.2", | |
"express-ws": "^5.0.2", | |
"pg": "^8.10.0", | |
"redis": "^4.6.6", | |
"tailwindcss": "^3.3.2" | |
}, | |
"devDependencies": { | |
"@types/express": "^4.17.17", | |
"@types/express-ws": "^3.0.1", | |
"@types/node": "^20.1.3", | |
"@types/pg": "^8.6.6", | |
"typescript": "^5.0.4" | |
}, | |
"scripts": { | |
"build": "tsc && tailwindcss -i input.css -o public/index.css", | |
"start": "node build/server.js" | |
} | |
} | |
EOF | |
RUN npm install | |
COPY <<-"EOF" tsconfig.json | |
{ | |
"compilerOptions": { | |
"outDir": "./build", | |
"allowJs": true, | |
"module": "commonjs", | |
"target": "ES2015", | |
"noImplicitAny": true, | |
"noImplicitThis": true, | |
"strictNullChecks": true, | |
"moduleResolution": "node", | |
"allowSyntheticDefaultImports": true | |
}, | |
"include": ["./*.ts"] | |
} | |
EOF | |
COPY <<-"EOF" server.ts | |
import { Client } from 'pg'; | |
import * as express from 'express'; | |
import * as redis from 'redis'; | |
import * as expressWs from 'express-ws'; | |
// set up express and web socket | |
const { app, getWss } = expressWs(express()); | |
const wss = getWss(); | |
// set up static content and ejs views | |
app.use(express.static('public')) | |
app.set('view engine', 'ejs'); | |
// common reconnect logic for postgres, redis clients | |
const reconnect = { | |
client: null as any, | |
interval: null as NodeJS.Timer | null, | |
connect: null as Function | null, | |
disconnect: null as Function | null, | |
reconnect() { | |
if (this.interval) return; | |
this.interval = setInterval(() => { this.tryConnect().catch(console.log) }, 1000); | |
}, | |
async tryConnect(reconnect = false) { | |
if (this.client || !this.connect || !this.disconnect) return; | |
try { | |
await this.connect(); | |
if (this.interval) { | |
clearInterval(this.interval); | |
this.interval = null; | |
} | |
} catch (error) { | |
console.error(error); | |
this.disconnect(); | |
if (reconnect) this.reconnect(); | |
throw(error); | |
} | |
} | |
} | |
// redis subscriber | |
const subscriber = { | |
...reconnect, | |
client: null as redis.RedisClientType | null, | |
async connect() { | |
this.client = redis.createClient({url: process.env.REDIS_URL}); | |
await this.client.connect(); | |
// Forward messages from redis to all websocket clients | |
this.client.subscribe('welcome:counter', (message: string) => { | |
count = parseInt(message); | |
wss.clients.forEach(client => { | |
try { client.send(message) } catch {}; | |
}); | |
}), | |
this.client.on('error', (err: object) => { | |
console.error('Redis Server Error', err); | |
this.disconnect(); | |
this.reconnect(); | |
}) | |
}, | |
disconnect() { | |
if (this.client) { | |
this.client.quit(); | |
this.client = null; | |
} | |
} | |
}; | |
// redis publisher | |
const publisher = { | |
...reconnect, | |
client: null as redis.RedisClientType | null, | |
async connect() { | |
this.client = redis.createClient({url: process.env.REDIS_URL}); | |
await this.client.connect(); | |
}, | |
disconnect() { | |
if (this.client) { | |
this.client.quit(); | |
this.client = null; | |
} | |
} | |
} | |
// postgres client | |
const postgres = { | |
...reconnect, | |
client: null as Client | null, | |
async connect() { | |
this.client = new Client({connectionString: process.env.DATABASE_URL}) | |
await this.client.connect(); | |
}, | |
disconnect() { | |
if (this.client) { | |
this.client.end(); | |
this.client = null; | |
} | |
} | |
} | |
// last known count | |
let count = 0; | |
// Main page | |
app.get('/', async (_request, response) => { | |
if (postgres.client) { | |
// get current count (may return zero rows) | |
let result = await postgres.client.query('SELECT "count" from "welcome"'); | |
// increment count, creating table row if necessary | |
if (!result.rows.length) { | |
count = 1; | |
await postgres.client?.query('INSERT INTO "welcome" VALUES($1)', [count]); | |
} else { | |
count = result.rows[0].count + 1; | |
await postgres.client?.query('UPDATE "welcome" SET "count" = $1', [count]); | |
} | |
// publish new count on redis | |
publisher.client?.publish('welcome:counter', count.toString()); | |
} | |
// render HTML response | |
response.render('index', { count }); | |
}); | |
// Define web socket route | |
app.ws('/websocket', (ws) => { | |
// update client on a best effort basis | |
try { ws.send(count.toString()) } catch {}; | |
// We don’t expect any messages on websocket, but log any ones we do get. | |
ws.on('message', console.log); | |
}); | |
(async () => { | |
// try to connect to each service | |
await Promise.all([ | |
subscriber.tryConnect(true), | |
publisher.tryConnect(true), | |
postgres.tryConnect(true) | |
]); | |
// Ensure welcome table exists | |
await postgres.client?.query('CREATE TABLE IF NOT EXISTS "welcome" ( "count" INTEGER )'); | |
// Start web server on port 3000 | |
app.listen(3000); | |
console.log('Server is listening on port 3000'); | |
})(); | |
EOF | |
COPY <<-"EOF" views/index.ejs | |
<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset="UTF-8"> | |
<link rel="shortcut icon" type="image/x-icon" href="https://fly.io/static/images/favicon/favicon.ico"> | |
<link href="index.css" rel="stylesheet"> | |
<script src="client.js" defer></script> | |
</head> | |
<body> | |
<div class="absolute top-0 left-0 h-screen w-screen mx-auto mb-3 bg-navy px-20 py-14 rounded-[20vh] flex flex-row items-center justify-center" style="background-color:rgb(36 24 91)"> | |
<img src="https://fly.io/static/images/brand/brandmark-light.svg" class="h-[50vh]" style="margin-top: -15px" alt="The monochrome white Fly.io brandmark on a navy background" srcset=""> | |
<div class="text-white" style="font-size: 40vh; padding: 10vh" id="counter"> | |
<%= count %> | |
</div> | |
</div> | |
</body> | |
</html> | |
EOF | |
COPY <<-"EOF" public/client.js | |
let ws = null; | |
let interval = null; | |
let counter = document.getElementById('counter'); | |
function openws() { | |
if (ws) return; | |
let url = window.location.protocol.replace('http', 'ws') + | |
'//' + window.location.host + '/websocket'; | |
ws = new WebSocket(url); | |
ws.onopen = () => { | |
if (interval) { | |
console.log('reconnected'); | |
clearInterval(interval); | |
interval = null; | |
} | |
}; | |
ws.onerror = error => { | |
console.error(error); | |
if (!interval) interval = setInterval(openws, 500); | |
}; | |
ws.onclose = () => { | |
console.log('disconnected'); | |
ws = null; | |
if (!interval) interval = setInterval(openws, 500); | |
}; | |
ws.onmessage = event => { | |
counter.textContent = event.data; | |
}; | |
}; | |
document.addEventListener("DOMContentLoaded", openws); | |
EOF | |
COPY <<-"EOF" tailwind.config.js | |
module.exports = { | |
content: ["./views/**/*.ejs"], | |
theme: { | |
extend: {}, | |
}, | |
plugins: [], | |
} | |
EOF | |
COPY <<-"EOF" input.css | |
@tailwind base; | |
@tailwind components; | |
@tailwind utilities; | |
EOF | |
RUN npm run build | |
EXPOSE 3000 | |
CMD ["npm", "run", "start" ] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment