Skip to content

Commit ced81c4

Browse files
committed
Add pub-sub history rewind example
1 parent c932f95 commit ced81c4

27 files changed

+5498
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
VITE_PUBLIC_ABLY_KEY=
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Rewind message history with Pub/Sub
2+
3+
This folder contains the code for rewinding message history (Typescript) - a demo of how you can leverage [Ably Pub/Sub](https://ably.com/docs/products/channels)'s channels to retrieve a set number of previously published messages within that channel using rewind.
4+
5+
## Getting started
6+
7+
1. Clone the [Ably docs](https://github.com/ably/docs) repository where this example can be found:
8+
9+
```sh
10+
git clone [email protected]:ably/docs.git
11+
```
12+
13+
2. Change directory:
14+
15+
```sh
16+
cd /examples/pub-sub-history-rewind/javascript/
17+
```
18+
19+
3. Rename the environment file:
20+
21+
```sh
22+
mv .env.example .env.local
23+
```
24+
25+
4. In `.env.local` update the value of `VITE_PUBLIC_ABLY_KEY` to be your Ably API key.
26+
27+
5. Install dependencies:
28+
29+
```sh
30+
yarn install
31+
```
32+
33+
6. Run the server:
34+
35+
```sh
36+
yarn run dev
37+
```
38+
39+
7. Try it out by opening a tab to [http://localhost:5173/](http://localhost:5173/) with your browser to see the result.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<link href='https://fonts.googleapis.com/css?family=Inter' rel='stylesheet'>
7+
<link rel="stylesheet" href="src/styles.css" />
8+
<title>Pub/Sub rewind message history</title>
9+
</head>
10+
<body class="font-inter">
11+
<div id="landing-page" class="min-h-screen flex items-center justify-center bg-gray-100">
12+
<div class="bg-white p-8 rounded-lg shadow-lg w-96">
13+
<h2 class="text-2xl mb-6 text-center">Live Football League Odds</h2>
14+
<p>Watch real-time odds movement for today's Football League match.</p>
15+
<div class="flex flex-col gap-4">
16+
<button
17+
id="pre-load-odds"
18+
class="
19+
text-white px-4 py-2 rounded
20+
bg-green-500 hover:bg-green-600
21+
">Load Live Match Odds</button>
22+
</div>
23+
</div>
24+
</div>
25+
<div id="game" class="min-h-screen bg-gray-100 p-8" style="display: none;">
26+
<div class="max-w-4xl mx-auto">
27+
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
28+
<div class="flex justify-between items-center text-2xl font-bold">
29+
<span>
30+
<span class="hidden sm:inline">Royal Knights</span>
31+
<span class="sm:hidden">R K</span>
32+
</span>
33+
<span id="score" class="bg-gray-800 text-white px-4 py-2 rounded">0-0</span>
34+
<span>
35+
<span class="hidden sm:inline">North Rangers</span>
36+
<span class="sm:hidden">N R</span>
37+
</span>
38+
</div>
39+
</div>
40+
<div class="grid grid-cols-3 gap-6 mb-8">
41+
<div class="bg-white rounded-lg shadow p-4">
42+
<h3 class="text-lg font-semibold mb-2">Home Win</h3>
43+
<p id="current-home" class="text-3xl font-bold text-green-600">2.50</p>
44+
</div>
45+
<div class="bg-white rounded-lg shadow p-4">
46+
<h3 class="text-lg font-semibold mb-2">Draw</h3>
47+
<p id="current-draw" class="text-3xl font-bold text-blue-600">3.42</p>
48+
</div>
49+
<div class="bg-white rounded-lg shadow p-4">
50+
<h3 class="text-lg font-semibold mb-2">Away Win</h3>
51+
<p id="current-away" class="text-3xl font-bold text-red-600">2.87</p>
52+
</div>
53+
</div>
54+
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
55+
<h2 class="text-xl font-bold mb-4">Next Goal</h2>
56+
<div class="grid grid-cols-3 gap-4">
57+
<div class="bg-gray-50 p-4 rounded">
58+
<h3 class="font-medium mb-2">Royal Knights</h3>
59+
<p id="next-goal-home" class="text-2xl font-bold">1.99</p>
60+
</div>
61+
<div class="bg-gray-50 p-4 rounded">
62+
<h3 class="font-medium mb-2">North Rangers</h3>
63+
<p id="next-goal-away" class="text-2xl font-bold">1.91</p>
64+
</div>
65+
<div class="bg-gray-50 p-4 rounded">
66+
<h3 class="font-medium mb-2">No Goal</h3>
67+
<p id="next-goal-none" class="text-2xl font-bold">2.66</p>
68+
</div>
69+
</div>
70+
</div>
71+
<div class="bg-white rounded-lg shadow-lg p-6">
72+
<h2 class="text-xl font-bold mb-4">Odds Movement History</h2>
73+
<div id="history" class="space-y-4">
74+
<div class="border-b pb-2">
75+
<div class="flex justify-between text-sm text-gray-600">
76+
<span>Home: 2.50</span>
77+
<span>Draw: 3.42</span>
78+
<span>Away: 2.87</span>
79+
<span>11:38:06</span>
80+
</div>
81+
</div>
82+
</div>
83+
</div>
84+
</div>
85+
</div>
86+
<script type="module" src="src/script.ts"></script>
87+
</body>
88+
</html>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"name": "js-pub-sub-history-rewind",
3+
"version": "1.0.0",
4+
"main": "index.js",
5+
"license": "MIT",
6+
"scripts": {
7+
"dev": "vite",
8+
"build": "tsc && vite build",
9+
"preview": "vite preview"
10+
},
11+
"devDependencies": {
12+
"autoprefixer": "^10.4.20",
13+
"dotenv": "^16.4.5",
14+
"postcss": "^8.4.47",
15+
"tailwindcss": "^3.4.13",
16+
"typescript": "^5.6.3",
17+
"@faker-js/faker": "^9.2.0"
18+
},
19+
"dependencies": {
20+
"ably": "^2.3.1",
21+
"nanoid": "^5.0.7",
22+
"vite": "^5.4.2"
23+
}
24+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import * as Ably from 'ably';
2+
import type { Message } from 'ably';
3+
import { faker } from '@faker-js/faker';
4+
import './styles.css';
5+
6+
interface MatchOdds {
7+
match: {
8+
homeTeam: string;
9+
awayTeam: string;
10+
timestamp: string;
11+
score: string;
12+
matchOdds: {
13+
homeWin: string;
14+
draw: string;
15+
awayWin: string;
16+
};
17+
nextGoal: {
18+
[key: string]: string;
19+
};
20+
};
21+
}
22+
23+
let matchData: MatchOdds | null = {
24+
match: {
25+
homeTeam: 'Royal Knights',
26+
awayTeam: 'North Rangers',
27+
timestamp: new Date().toISOString(),
28+
score: '0-0',
29+
matchOdds: {
30+
homeWin: '2.45',
31+
draw: '3.25',
32+
awayWin: '2.85',
33+
},
34+
nextGoal: {
35+
'Royal Knights': '1.95',
36+
'North Rangers': '1.85',
37+
'No Goal': '2.75',
38+
},
39+
},
40+
};
41+
42+
const preloadButton = document.getElementById('pre-load-odds') as HTMLButtonElement;
43+
const urlParams = new URLSearchParams(window.location.search);
44+
const channelName = urlParams.get('name') || 'pub-sub-history-rewind';
45+
const landingPage = document.getElementById('landing-page');
46+
const game = document.getElementById('game');
47+
let channel: Ably.RealtimeChannel | null = null;
48+
49+
async function enterGame() {
50+
landingPage.style.display = 'none';
51+
game.style.display = 'block';
52+
53+
const client = new Ably.Realtime({
54+
key: import.meta.env.VITE_PUBLIC_ABLY_KEY as string,
55+
clientId: faker.person.firstName(),
56+
});
57+
58+
channel = client.channels.get(channelName, {
59+
params: { rewind: '10' },
60+
});
61+
62+
channel.subscribe(async (message) => {
63+
matchData = message.data;
64+
await addHistoryItem(message);
65+
await updateCurrentOdds(message);
66+
});
67+
68+
await updateRandomOdds();
69+
}
70+
71+
preloadButton.addEventListener('click', async () => {
72+
preloadButton.disabled = true;
73+
const client = new Ably.Realtime({
74+
key: import.meta.env.VITE_PUBLIC_ABLY_KEY as string,
75+
clientId: faker.person.firstName(),
76+
});
77+
78+
const channel = client.channels.get(channelName);
79+
80+
for (let i = 0; i < 10; i++) {
81+
const markets = ['homeWin', 'draw', 'awayWin', 'nextGoal'];
82+
const numMarketsToUpdate = Math.floor(Math.random() * 2) + 1;
83+
const marketsToUpdate = markets.sort(() => 0.5 - Math.random()).slice(0, numMarketsToUpdate);
84+
85+
marketsToUpdate.forEach((market) => {
86+
if (market === 'nextGoal') {
87+
const team = Object.keys(matchData.match.nextGoal)[Math.floor(Math.random() * 3)];
88+
matchData.match.nextGoal[team] = (parseFloat(matchData.match.nextGoal[team]) + (Math.random() * 0.2 - 0.1)).toFixed(2);
89+
} else {
90+
matchData.match.matchOdds[market] = (parseFloat(matchData.match.matchOdds[market]) + (Math.random() * 0.2 - 0.1)).toFixed(2);
91+
}
92+
});
93+
94+
matchData.match.timestamp = new Date().toISOString();
95+
await channel.publish('odds', matchData);
96+
97+
// Show alert for each publish
98+
const alert = document.createElement('div');
99+
alert.className =
100+
'fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow-lg transition-opacity duration-500';
101+
alert.textContent = `Update ${i + 1}/10: New odds published`;
102+
document.body.appendChild(alert);
103+
104+
// Remove alert after 2 seconds
105+
setTimeout(() => {
106+
alert.style.opacity = '0';
107+
setTimeout(() => alert.remove(), 500);
108+
}, 2000);
109+
110+
await new Promise((resolve) => setTimeout(resolve, 1000));
111+
}
112+
113+
await enterGame();
114+
});
115+
116+
async function updateCurrentOdds(message: Message) {
117+
const score = document.getElementById('score');
118+
score.textContent = message.data.match.score;
119+
const currentHome = document.getElementById('current-home');
120+
currentHome.textContent = message.data.match.matchOdds.homeWin;
121+
const currentAway = document.getElementById('current-away');
122+
currentAway.textContent = message.data.match.matchOdds.awayWin;
123+
const currentDraw = document.getElementById('current-draw');
124+
currentDraw.textContent = message.data.match.matchOdds.draw;
125+
const nextGoalHome = document.getElementById('next-goal-home');
126+
nextGoalHome.textContent = message.data.match.nextGoal[message.data.match.homeTeam];
127+
const nextGoalAway = document.getElementById('next-goal-away');
128+
nextGoalAway.textContent = message.data.match.nextGoal[message.data.match.awayTeam];
129+
const nextGoalNoGoal = document.getElementById('next-goal-none');
130+
nextGoalNoGoal.textContent = message.data.match.nextGoal['No Goal'];
131+
}
132+
133+
async function addHistoryItem(message: Message, position = 'prepend') {
134+
const history = document.getElementById('history');
135+
const historyItem = document.createElement('div');
136+
historyItem.id = `history-item-${message.id}`;
137+
historyItem.className = 'border-b pb-2';
138+
const historyDiv = document.createElement('div');
139+
historyDiv.className = 'flex justify-between text-sm text-gray-600';
140+
historyItem.appendChild(historyDiv);
141+
142+
const homeWin = document.createElement('span');
143+
homeWin.textContent = `Home: ${message.data.match.matchOdds.homeWin}`;
144+
const draw = document.createElement('span');
145+
draw.textContent = `Draw: ${message.data.match.matchOdds.draw}`;
146+
const awayWin = document.createElement('span');
147+
awayWin.textContent = `Away: ${message.data.match.matchOdds.awayWin}`;
148+
const time = document.createElement('span');
149+
time.textContent = new Date(message.data.match.timestamp).toLocaleTimeString([], {
150+
hour: '2-digit',
151+
minute: '2-digit',
152+
second: '2-digit',
153+
});
154+
historyDiv.appendChild(homeWin);
155+
historyDiv.appendChild(draw);
156+
historyDiv.appendChild(awayWin);
157+
historyDiv.appendChild(time);
158+
159+
if (position === 'prepend') {
160+
history.prepend(historyItem);
161+
} else {
162+
history.appendChild(historyItem);
163+
}
164+
}
165+
166+
async function updateRandomOdds() {
167+
if (!matchData) {
168+
return;
169+
}
170+
171+
for (let i = 0; i < 20; i++) {
172+
const delayTime = 5000;
173+
await new Promise((resolve) => {
174+
setTimeout(resolve, delayTime);
175+
});
176+
177+
const markets = ['homeWin', 'draw', 'awayWin', 'nextGoal'];
178+
const numMarketsToUpdate = Math.floor(Math.random() * 3) + 1;
179+
const marketsToUpdate = markets.sort(() => 0.5 - Math.random()).slice(0, numMarketsToUpdate);
180+
181+
const newOdds = { ...matchData };
182+
183+
marketsToUpdate.forEach((market) => {
184+
if (market === 'nextGoal') {
185+
const team = Object.keys(newOdds.match.nextGoal)[Math.floor(Math.random() * 3)];
186+
newOdds.match.nextGoal[team] = (parseFloat(newOdds.match.nextGoal[team]) + (Math.random() * 0.2 - 0.1)).toFixed(2);
187+
} else {
188+
newOdds.match.matchOdds[market] = (parseFloat(newOdds.match.matchOdds[market]) + (Math.random() * 0.2 - 0.1)).toFixed(2);
189+
}
190+
});
191+
192+
newOdds.match.timestamp = new Date().toISOString();
193+
await channel.publish('odds', newOdds);
194+
}
195+
}

0 commit comments

Comments
 (0)