-
Notifications
You must be signed in to change notification settings - Fork 43
[EDU-1691] - Add pub-sub rewind example #2436
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
VITE_PUBLIC_ABLY_KEY= |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. | ||
|
||
# dependencies | ||
/node_modules | ||
/.pnp | ||
.pnp.js | ||
.yarn/install-state.gz | ||
|
||
# testing | ||
/coverage | ||
|
||
# next.js | ||
/.next/ | ||
/out/ | ||
|
||
# production | ||
/build | ||
|
||
# misc | ||
.DS_Store | ||
*.pem | ||
|
||
# debug | ||
npm-debug.log* | ||
yarn-debug.log* | ||
yarn-error.log* | ||
|
||
# local env files | ||
.env*.local | ||
|
||
# vercel | ||
.vercel | ||
|
||
# typescript | ||
*.tsbuildinfo | ||
next-env.d.ts |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
# Rewind channel option with Pub/Sub | ||
|
||
Rewind is a Pub/Sub channel option that enables users to retrieve a set number of historical messages published to an application. | ||
|
||
Use rewind to retrieve a pre-determined number of messages that have been previously published to a channel when attached. Users can then start working in your application with context of what happened before they went joined, or came online. Uses include retrieving the history of odds on a football game before providing the last 2 minutes worth of contextual data in a realtime dashboard. | ||
|
||
Rewind enables users to retrieve a set number of messages that have been previously published within an application. It enables provides users with context as to how the current state has been reached. | ||
|
||
Rewind is a channel option which is implemented using [Ably Pub/Sub](https://ably.com/docs/products/channels). The Pub/Sub SDK provides a set of flexible APIs capable of building any realtime application. It is powered by Ably's reliable and scalable platform. | ||
|
||
## Resources | ||
|
||
Use the following methods to specify where to start an attachment from when attaching to a channel in a pub/sub application: | ||
|
||
* [`channel.get()`](https://ably.com/docs/channels#create) - creates a new or retrieves an existing `channel`. Using the `rewind` channel option retrieves the set number of historical messages published to the channel when the channel is attached. | ||
* [`channel.subscribe()`](https://ably.com/docs/channels#subscribe) - subscribes to message events within a specific channel by registering a listener. Message events are emitted when a user publishes a message. | ||
|
||
Find out more about [rewind](https://ably.com/docs/channels/options/rewind). | ||
|
||
## Getting started | ||
|
||
1. Clone the [Ably docs](https://github.com/ably/docs) repository where this example can be found: | ||
|
||
```sh | ||
git clone [email protected]:ably/docs.git | ||
``` | ||
|
||
2. Change directory: | ||
|
||
```sh | ||
cd /examples/ | ||
``` | ||
|
||
3. Rename the environment file: | ||
|
||
```sh | ||
mv .env.example .env.local | ||
``` | ||
|
||
4. In `.env.local` update the value of `VITE_ABLY_KEY` to be your Ably API key. | ||
|
||
5. Install dependencies: | ||
|
||
```sh | ||
yarn install | ||
``` | ||
|
||
6. Run the server: | ||
|
||
```sh | ||
yarn run pub-sub-rewind-javascript | ||
``` | ||
|
||
7. Try it out by opening a tab to [http://localhost:5173/](http://localhost:5173/) with your browser to see the result. | ||
|
||
## Open in CodeSandbox | ||
|
||
In CodeSandbox, rename the `.env.example` file to `.env.local` and update the value of your `VITE_PUBLIC_ABLY_KEY` variable to use your Ably API key. |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,60 @@ | ||||||
<!DOCTYPE html> | ||||||
<html lang="en"> | ||||||
<head> | ||||||
<meta charset="UTF-8"> | ||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||||||
<link href='https://fonts.googleapis.com/css?family=Inter' rel='stylesheet'> | ||||||
<link rel="stylesheet" href="src/styles.css" /> | ||||||
<title>Pub/Sub rewind channel</title> | ||||||
</head> | ||||||
<body class="font-inter"> | ||||||
<div id="landing-page" class="min-h-screen flex items-center justify-center bg-gray-100"> | ||||||
<div class="bg-white p-8 rounded-lg shadow-lg w-96"> | ||||||
<h2 class="text-2xl mb-6 text-center">Live Football League Odds</h2> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
<p class="text-sm text-center">Watch real-time odds movement for today's Football League match.</p> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Maybe something like this to show what's happening? |
||||||
<div class="flex flex-col gap-2 mt-4"> | ||||||
<button | ||||||
id="pre-load-odds" | ||||||
class="uk-btn uk-btn-primary uk-border-rounded-right whitespace-nowrap">Load Live Match Odds</button> | ||||||
</div> | ||||||
</div> | ||||||
</div> | ||||||
<div id="game" class="min-h-screen bg-gray-100 p-4" style="display: none;"> | ||||||
<div class="max-w-sm mx-auto"> | ||||||
<div class="bg-white rounded shadow p-3 mb-4"> | ||||||
<div class="flex justify-between items-center text-sm font-semibold"> | ||||||
<span> | ||||||
<span class="hidden sm:inline">Royal Knights</span> | ||||||
<span class="sm:hidden">R K</span> | ||||||
</span> | ||||||
<span id="score" class="bg-gray-800 text-white px-2 py-1 rounded">0-0</span> | ||||||
<span> | ||||||
<span class="hidden sm:inline">North Rangers</span> | ||||||
<span class="sm:hidden">N R</span> | ||||||
</span> | ||||||
</div> | ||||||
</div> | ||||||
<div class="grid grid-cols-3 gap-2 mb-4"> | ||||||
<div class="bg-white rounded shadow p-2"> | ||||||
<h3 class="text-xs font-medium mb-1">Home Win</h3> | ||||||
<p id="current-home" class="text-lg font-bold text-green-600">2.50</p> | ||||||
</div> | ||||||
<div class="bg-white rounded shadow p-2"> | ||||||
<h3 class="text-xs font-medium mb-1">Draw</h3> | ||||||
<p id="current-draw" class="text-lg font-bold text-blue-600">3.42</p> | ||||||
</div> | ||||||
<div class="bg-white rounded shadow p-2"> | ||||||
<h3 class="text-xs font-medium mb-1">Away Win</h3> | ||||||
<p id="current-away" class="text-lg font-bold text-red-600">2.87</p> | ||||||
</div> | ||||||
</div> | ||||||
<div class="bg-white rounded shadow p-3"> | ||||||
<h2 class="text-sm font-bold mb-2">Odds Movement History</h2> | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
<div id="history" class="space-y-2"> | ||||||
</div> | ||||||
</div> | ||||||
</div> | ||||||
</div> | ||||||
<script type="module" src="src/script.ts"></script> | ||||||
</body> | ||||||
</html> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
{ | ||
"name": "pub-sub-rewind-javascript", | ||
"version": "1.0.0", | ||
"main": "index.js", | ||
"license": "MIT", | ||
"scripts": { | ||
"dev": "vite", | ||
"build": "tsc && vite build", | ||
"preview": "vite preview" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
import * as Ably from 'ably'; | ||
import type { Message } from 'ably'; | ||
import minifaker from 'minifaker'; | ||
import './styles.css'; | ||
|
||
import 'minifaker/locales/en'; | ||
|
||
interface MatchOdds { | ||
match: { | ||
homeTeam: string; | ||
awayTeam: string; | ||
timestamp: number; | ||
score: string; | ||
matchOdds: { | ||
homeWin: string; | ||
draw: string; | ||
awayWin: string; | ||
}; | ||
}; | ||
} | ||
|
||
let matchData: MatchOdds | null = { | ||
match: { | ||
homeTeam: 'Royal Knights', | ||
awayTeam: 'North Rangers', | ||
timestamp: Date.now(), | ||
score: '0-0', | ||
matchOdds: { | ||
homeWin: '2.45', | ||
draw: '3.25', | ||
awayWin: '2.85', | ||
}, | ||
}, | ||
}; | ||
|
||
const preloadButton = document.getElementById('pre-load-odds') as HTMLButtonElement; | ||
const urlParams = new URLSearchParams(window.location.search); | ||
const channelName = urlParams.get('name') || 'pub-sub-rewind'; | ||
const landingPage = document.getElementById('landing-page'); | ||
const game = document.getElementById('game'); | ||
let channel: Ably.RealtimeChannel | null = null; | ||
|
||
async function enterGame() { | ||
landingPage.style.display = 'none'; | ||
game.style.display = 'block'; | ||
|
||
const client = new Ably.Realtime({ | ||
key: import.meta.env.VITE_ABLY_KEY as string, | ||
clientId: minifaker.firstName(), | ||
}); | ||
|
||
channel = client.channels.get(channelName, { | ||
params: { rewind: '10' }, | ||
}); | ||
|
||
channel.subscribe(async (message) => { | ||
matchData = message.data; | ||
await addHistoryItem(message); | ||
await updateCurrentOdds(message); | ||
}); | ||
|
||
await updateRandomOdds(); | ||
} | ||
|
||
preloadButton.addEventListener('click', async () => { | ||
preloadButton.disabled = true; | ||
const client = new Ably.Realtime({ | ||
key: import.meta.env.VITE_ABLY_KEY as string, | ||
clientId: minifaker.firstName(), | ||
}); | ||
|
||
const channel = client.channels.get(channelName); | ||
|
||
for (let i = 0; i < 10; i++) { | ||
const markets = ['homeWin', 'draw', 'awayWin']; | ||
const numMarketsToUpdate = Math.floor(Math.random() * 2) + 1; | ||
const marketsToUpdate = markets.sort(() => 0.5 - Math.random()).slice(0, numMarketsToUpdate); | ||
|
||
marketsToUpdate.forEach((market) => { | ||
matchData.match.matchOdds[market] = (parseFloat(matchData.match.matchOdds[market]) + (Math.random() * 0.2 - 0.1)).toFixed(2); | ||
}); | ||
|
||
matchData.match.timestamp = Date.now(); | ||
await channel.publish('odds', matchData); | ||
|
||
// Show alert for each publish | ||
const alert = document.createElement('div'); | ||
alert.className = | ||
'fixed bottom-4 right-4 bg-green-500 text-white px-4 py-2 rounded shadow-lg transition-opacity duration-500'; | ||
alert.textContent = `Update ${i + 1}/10: New odds published`; | ||
document.body.appendChild(alert); | ||
|
||
// Remove alert after 2 seconds | ||
setTimeout(() => { | ||
alert.style.opacity = '0'; | ||
setTimeout(() => alert.remove(), 500); | ||
}, 2000); | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 1000)); | ||
} | ||
|
||
await enterGame(); | ||
}); | ||
|
||
async function updateCurrentOdds(message: Message) { | ||
const score = document.getElementById('score'); | ||
score.textContent = message.data.match.score; | ||
const currentHome = document.getElementById('current-home'); | ||
currentHome.textContent = message.data.match.matchOdds.homeWin; | ||
const currentAway = document.getElementById('current-away'); | ||
currentAway.textContent = message.data.match.matchOdds.awayWin; | ||
const currentDraw = document.getElementById('current-draw'); | ||
currentDraw.textContent = message.data.match.matchOdds.draw; | ||
} | ||
|
||
async function addHistoryItem(message: Message, position = 'prepend') { | ||
const history = document.getElementById('history'); | ||
const historyItem = document.createElement('div'); | ||
historyItem.id = `history-item-${message.id}`; | ||
historyItem.className = 'border-b pb-2'; | ||
const historyDiv = document.createElement('div'); | ||
historyDiv.className = 'flex justify-between text-sm text-gray-600'; | ||
historyItem.appendChild(historyDiv); | ||
|
||
const homeWin = document.createElement('span'); | ||
homeWin.textContent = `Home: ${message.data.match.matchOdds.homeWin}`; | ||
const draw = document.createElement('span'); | ||
draw.textContent = `Draw: ${message.data.match.matchOdds.draw}`; | ||
const awayWin = document.createElement('span'); | ||
awayWin.textContent = `Away: ${message.data.match.matchOdds.awayWin}`; | ||
const time = document.createElement('span'); | ||
const timestamp = new Date(message.data.match.timestamp); | ||
time.textContent = `${timestamp.getHours()}:${timestamp.getMinutes().toString().padStart(2, '0')}`; | ||
historyDiv.appendChild(homeWin); | ||
historyDiv.appendChild(draw); | ||
historyDiv.appendChild(awayWin); | ||
historyDiv.appendChild(time); | ||
|
||
if (position === 'prepend') { | ||
history.prepend(historyItem); | ||
} else { | ||
history.appendChild(historyItem); | ||
} | ||
} | ||
|
||
async function updateRandomOdds() { | ||
if (!matchData) { | ||
return; | ||
} | ||
|
||
for (let i = 0; i < 20; i++) { | ||
const delayTime = 5000; | ||
await new Promise((resolve) => setTimeout(resolve, delayTime)); | ||
|
||
const markets = ['homeWin', 'draw', 'awayWin']; | ||
const numMarketsToUpdate = Math.floor(Math.random() * 3) + 1; | ||
const marketsToUpdate = markets.sort(() => 0.5 - Math.random()).slice(0, numMarketsToUpdate); | ||
|
||
const newOdds = { ...matchData }; | ||
|
||
marketsToUpdate.forEach((market) => { | ||
newOdds.match.matchOdds[market] = (parseFloat(newOdds.match.matchOdds[market]) + (Math.random() * 0.2 - 0.1)).toFixed(2); | ||
}); | ||
|
||
newOdds.match.timestamp = Date.now(); | ||
await channel.publish('odds', newOdds); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.