Skip to content

Commit

Permalink
✨ server-sent events for story updates
Browse files Browse the repository at this point in the history
  • Loading branch information
haliphax committed Aug 14, 2023
1 parent 5e40e7b commit 4f4afe2
Show file tree
Hide file tree
Showing 24 changed files with 319 additions and 143 deletions.
11 changes: 9 additions & 2 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@
"parser": "@typescript-eslint/parser",
"sourceType": "module"
},
"plugins": ["vue", "prettier", "@typescript-eslint"],
"plugins": ["vue", "@typescript-eslint", "prettier"],
"rules": {
"indent": ["error", "tab", { "ignoredNodes": ["PropertyDefinition"] }],
"indent": [
"error",
"tab",
{
"ignoredNodes": ["PropertyDefinition"],
"offsetTernaryExpressions": true
}
],
"vue/multi-word-component-names": "off"
}
}
7 changes: 4 additions & 3 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"prettier.prettierPath": "./node_modules/prettier",
"prettier.useTabs": true,
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.format.enable": true,
"prettier.enable": true,
"prettier.documentSelectors": ["!**/*.ts"]
}
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"axios": "^1.3.5",
"buffer": "^5.7.1",
"eslint": "^8.39.0",
"eslint-config-prettier": "^8.8.0",
"eslint-config-prettier": "^8.10.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^9.11.0",
"gitmoji-cli": "^8.1.1",
Expand Down
38 changes: 11 additions & 27 deletions src/back-end/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,24 @@ import compression from "compression";
import historyApiFallback from "connect-history-api-fallback";
import cors from "cors";
import express from "express";
import { remultExpress } from "remult/remult-express";
import Participant from "../models/participant";
import Story from "../models/story";
import Vote from "../models/vote";
import glitchWebhook from "./glitch-webhook";
import routes from "./routes";
import server from "./server";

const host = process.env.host ?? "localhost";
const port = parseInt(process.env.port ?? "3000");

const app = express();

if (process.env.NODE_ENV !== "production") app.use(cors({ origin: "*" }));

const staticMiddleware = express.static("dist/front-end");
if (process.env.NODE_ENV !== "production") {
app.use(cors({ origin: "*" }));
}

app.use(compression());
app.use(staticMiddleware);
app.use(express.json());
app.use(server);
routes(app);
app.use(historyApiFallback());
app.use(staticMiddleware);
app.use(
remultExpress({
entities: [Participant, Story, Vote],
async initApi(remult) {
const repo = remult.repo(Story);
app.use(express.static("dist/front-end"));

if ((await repo.count()) === 0) {
await repo.insert([{ id: "1", title: "Testing this thing" }]);
}
},
})
app.listen(port, host, () =>
console.log(`Server listening at http://${host}:${port}`)
);

glitchWebhook(app);

app.listen(port, host, () => {
console.log(`Server listening at http://${host}:${port}`);
});
14 changes: 14 additions & 0 deletions src/back-end/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Application } from "express";
import glitchWebhook from "./routes/glitch-webhook";
import reveal from "./routes/reveal";
import { storySSE } from "./routes/story-sse";
import vote from "./routes/vote";

const routes = async (app: Application) => {
glitchWebhook(app);
reveal(app);
storySSE(app);
vote(app);
};

export default routes;
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import bodyParser from "body-parser";
import { execSync } from "child_process";
import { createHmac, timingSafeEqual } from "crypto";
import { Application, Request, Response } from "express";

/** Adds webhook endpoint for updating from remote git */
const glitchWebhook = (app: Application) => {
app.use(bodyParser.json());
app.post("/git", (req: Request, res: Response) => {
if (!process.env.SECRET) return res.sendStatus(500);
if (!process.env.SECRET) {
return res.sendStatus(500);
}

const hmac = createHmac("sha1", process.env.SECRET);
const sig = `sha1=${hmac.update(JSON.stringify(req.body)).digest("hex")}`;
Expand All @@ -19,8 +19,7 @@ const glitchWebhook = (app: Application) => {
)
) {
console.error("webhook signature incorrect");
res.sendStatus(403);
return;
return res.sendStatus(403);
}

res.sendStatus(200);
Expand Down
24 changes: 24 additions & 0 deletions src/back-end/routes/reveal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Application } from "express";
import { remult } from "remult";
import { Story } from "../../models/story";
import server from "../server";
import { updateStory } from "./story-sse";

const reveal = (app: Application) =>
app.post("/reveal/:story", server.withRemult, async (r, s) => {
const story = r.params.story;

console.log(`Revealing ${story}`);
await remult.repo(Story).update(story, { revealed: true });
s.sendStatus(202);

const storyObject = await remult.repo(Story).findId(story);

if (!storyObject) {
throw new Error("No story");
}

updateStory(storyObject);
});

export default reveal;
50 changes: 50 additions & 0 deletions src/back-end/routes/story-sse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { Application, Response } from "express";
import { v4 as uuid } from "uuid";
import { Story } from "../../models/story";

interface NarfClient {
id: string;
response: Response;
}

const clients = new Map<string, NarfClient[]>();

/** Server-sent events endpoint for story updates */
export const storySSE = (app: Application) => {
app.get("/story/:story/events", async (r, s) => {
s.set({
"Cache-Control": "no-cache",
"Content-Type": "text/event-stream",
Connection: "keep-alive",
});
s.flushHeaders();

const story = r.params.story;
const client: NarfClient = { id: uuid(), response: s };

console.log(`Client ${client.id} connected`);

if (clients.has(story)) {
clients.set(story, (clients.get(story) ?? []).concat(client));
} else {
clients.set(story, [client]);
}

r.on("close", () => {
console.log(`Client ${client.id} disconnected`);
clients.set(
story,
(clients.get(story) ?? []).filter((c) => c.id !== client.id)
);
});
});
};

export const updateStory = (story: Story) => {
console.log(`Sending update to story ${story.id}`);
clients.get(story.id)?.map((c) => {
console.log(`Updating client ${c.id}`);
c.response.write(`data: ${JSON.stringify(story)}\n\n`);
c.response.flush();
});
};
48 changes: 48 additions & 0 deletions src/back-end/routes/vote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { Application } from "express";
import { remult } from "remult";
import { Story } from "../../models/story";
import { Vote } from "../../models/vote";
import server from "../server";
import { updateStory } from "./story-sse";

/** Route for accepting a vote submission for a story */
const vote = (app: Application) => {
app.put("/vote/:story", server.withRemult, async (r, s) => {
console.log(`Incoming vote for ${r.params.story}`);

const vote = r.body;
const story = await remult.repo(Story).findId(r.params.story);

if (!story) {
throw new Error("No such story");
}

const votes: Vote[] = [];

if (!story._votes) {
votes.push(vote);
} else {
let updated = false;

for (const v of story._votes) {
if (v.participant?.id === vote.participant?.id) {
updated = true;
votes.push(vote);
} else {
votes.push(v);
}
}

if (!updated) {
votes.push(vote);
}
}

story._votes = votes;
await remult.repo(Story).update(r.params.story, story);
s.sendStatus(201);
updateStory(story);
});
};

export default vote;
28 changes: 28 additions & 0 deletions src/back-end/server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { remultExpress } from "remult/remult-express";
import { Story } from "../models/story";
import { FIBONACCI } from "../scales";

const server = remultExpress({
entities: [Story],
async initApi(remult) {
const storyRepo = remult.repo(Story);

if ((await storyRepo.count()) === 0) {
await storyRepo.insert({
id: "1",
title: "Testing this thing",
_votes: [
{
participant: {
id: "test",
name: "Test Participant",
},
vote: FIBONACCI[0],
},
],
});
}
},
});

export default server;
4 changes: 2 additions & 2 deletions src/declarations/vuex.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Store } from "vuex";
import { storeState } from "../front-end/scripts/types";
import { StoreState } from "../front-end/scripts/types";

declare module "@vue/runtime-core" {
interface ComponentCustomProperties {
$store: Store<storeState>;
$store: Store<StoreState>;
}
}
4 changes: 1 addition & 3 deletions src/front-end/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>narf!</title>
<link rel="icon" type="image/gif" href="data-url:favicon.gif" />
<style>
@import "styles/index.less";
</style>
<link rel="stylesheet" href="styles/index.less" />
</head>
<body>
<div id="app"></div>
Expand Down
2 changes: 1 addition & 1 deletion src/front-end/scripts/components/actions.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default Actions;
<template>
<ul class="unstyled">
<li>
<button :disabled="$store.state.story.revealed" @click="reveal">
<button :disabled="$store.state.story.story?.revealed" @click="reveal">
Reveal
</button>
</li>
Expand Down
Loading

0 comments on commit 4f4afe2

Please sign in to comment.