Skip to content

Instantly share code, notes, and snippets.

@rubys
Created May 12, 2023 03:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rubys/a88964e20c7a24172d84ddcfc47bb76e to your computer and use it in GitHub Desktop.
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
# 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