Skip to content

[PUB-1135] Add LiveObjects examples (LiveMap and LiveCounter) #2437

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

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added examples/ably-2.6.5-liveobjects.tgz
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ably-js liveobjects is not published on npm yet, so local build of ably-js on liveobjects branch is provided to be able to test the examples

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid merging into main until ably-js is up to date to cover the LiveObjects work. Do we have any idea when this is expected?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should avoid merging into main until ably-js

No problem, will wait.
npm release will most likely be mid next week

Binary file not shown.
36 changes: 36 additions & 0 deletions examples/liveobjects-live-counter/javascript/.gitignore
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
62 changes: 62 additions & 0 deletions examples/liveobjects-live-counter/javascript/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Synchronizing numeric values with LiveCounter

Enable clients to update and synchronize numerical values in an application in realtime.

LiveCounter is a synchronized numerical counter that supports increment and decrement operations. It ensures that all updates are correctly applied and synchronized across users in realtime, preventing inconsistencies when multiple users modify the counter value simultaneously.

LiveCounter is useful for tracking values that need to be updated dynamically, such as votes, reaction counts, live leaderboards, game stats, or other numeric data points.

LiveCounter is implemented using [Ably LiveObjects](/docs/liveobjects). LiveObjects, as a feature of [Ably Pub/Sub](/docs/channels), contains a set of purpose-built APIs that abstract away the complexities of managing shared state between clients in an application. It is built on top of Ably's core platform, and so it provides the same performance guarantees and scaling potential.

## Resources

Use the following methods to interact with a LiveCounter in your application:

- [`objects.getRoot()`](https://ably.com/docs/sdk/js/v2.0/interfaces/ably.Objects.html#getRoot): retrieves the root object that serves as the starting point for storing and organizing objects on a channel.
- [`objects.createCounter()`](https://ably.com/docs/sdk/js/v2.0/interfaces/ably.Objects.html#createCounter): creates a new LiveCounter instance.
- [`liveCounter.value()`](https://ably.com/docs/sdk/js/v2.0/interfaces/ably.LiveCounter.html#value): returns the current value of a counter.
- [`liveCounter.increment()`](https://ably.com/docs/sdk/js/v2.0/interfaces/ably.LiveCounter.html#increment): sends the operation message to the Ably system to increase the counter value.
- [`liveCounter.decrement()`](https://ably.com/docs/sdk/js/v2.0/interfaces/ably.LiveCounter.html#decrement): sends the operation message to the Ably system to decrease the counter value.
- [`liveCounter.subscribe()`](https://ably.com/docs/sdk/js/v2.0/interfaces/ably.LiveCounter.html#subscribe): subscribes to LiveCounter updates by registering a listener.

Find out more about [LiveCounter](/docs/liveobjects/counter).

## 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 liveobjects-live-counter-javascript
```

7. Try it out by opening two tabs 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_ABLY_KEY` variable to use your Ably API key.
55 changes: 55 additions & 0 deletions examples/liveobjects-live-counter/javascript/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<!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>LiveCounter example</title>
</head>
<body class="font-inter">
<div class="min-h-screen p-8">
<div class="max-w-sm mx-auto bg-white rounded-lg shadow-lg p-6 space-y-2">
<h2 class="text-xl font-bold mb-4">Vote for your favorite Color</h2>

<div id="vote-options">
<div class="flex justify-between items-center p-2 border-b space-x-4">
<span class="text-red-500 font-semibold flex-grow">Red</span>
<span class="font-bold text-gray-700" id="count-red">0</span>
<button
class="uk-btn uk-btn-primary uk-border-rounded-right whitespace-nowrap vote-button"
data-color="red"
>
Vote
</button>
</div>

<div class="flex justify-between items-center p-2 border-b space-x-4">
<span class="text-green-500 font-semibold flex-grow">Green</span>
<span class="font-bold text-gray-700" id="count-green">0</span>
<button
class="uk-btn uk-btn-primary uk-border-rounded-right whitespace-nowrap vote-button"
data-color="green"
>
Vote
</button>
</div>

<div class="flex justify-between items-center p-2 space-x-4">
<span class="text-blue-500 font-semibold flex-grow">Blue</span>
<span class="font-bold text-gray-700" id="count-blue">0</span>
<button
class="uk-btn uk-btn-primary uk-border-rounded-right whitespace-nowrap vote-button"
data-color="blue"
>
Vote
</button>
</div>
</div>

<button class="uk-btn uk-btn-secondary uk-border-rounded-right" id="reset">Reset</button>
</div>
</div>
<script type="module" src="src/script.ts"></script>
</body>
</html>
11 changes: 11 additions & 0 deletions examples/liveobjects-live-counter/javascript/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "liveobjects-live-counter-javascript",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
}
}
12 changes: 12 additions & 0 deletions examples/liveobjects-live-counter/javascript/src/ably.config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { LiveCounter } from 'ably';
import { Color } from './script';

declare global {
export interface ObjectsTypes {
root: {
[Color.red]: LiveCounter;
[Color.green]: LiveCounter;
[Color.blue]: LiveCounter;
};
}
}
92 changes: 92 additions & 0 deletions examples/liveobjects-live-counter/javascript/src/script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { DefaultRoot, LiveCounter, LiveMap, Realtime } from 'ably';
import Objects from 'ably/objects';
import { nanoid } from 'nanoid';
import './styles.css';

export enum Color {
red = 'red',
green = 'green',
blue = 'blue',
}

const client = new Realtime({
clientId: nanoid(),
key: import.meta.env.VITE_ABLY_KEY as string,
environment: 'sandbox',
plugins: { Objects },
});

const urlParams = new URLSearchParams(window.location.search);

const channelName = urlParams.get('name') || 'objects-live-counter';
const channel = client.channels.get(channelName, { modes: ['OBJECT_PUBLISH', 'OBJECT_SUBSCRIBE'] });

const colorCountDivs: Record<Color, HTMLElement> = {
red: document.getElementById('count-red')!,
green: document.getElementById('count-green')!,
blue: document.getElementById('count-blue')!,
};
const countersReset = document.getElementById('reset')!;

async function main() {
await channel.attach();

const objects = channel.objects;
const root = await objects.getRoot();

await initCounters(root);
addEventListenersToButtons(root);
}

async function initCounters(root: LiveMap<DefaultRoot>) {
// subscribe to root to get notified when counter objects get changed on the root.
// for example, when we reset all counters
root.subscribe(({ update }) => {
Object.entries(update).forEach(([keyName, change]) => {
if (change === 'removed') {
return;
}

if (Object.values(Color).includes(keyName as Color)) {
// key pointing to a counter object got updated, resubscribe to a counter
const color = keyName as Color;
subscribeToCounterUpdates(color, root.get(color)!);
}
});
});

await Promise.all(
Object.values(Color).map(async (color) => {
if (root.get(color)) {
subscribeToCounterUpdates(color, root.get(color)!);
return;
}

await root.set(color, await channel.objects.createCounter());
}),
);
}

function subscribeToCounterUpdates(color: Color, counter: LiveCounter) {
counter.subscribe(() => {
colorCountDivs[color].innerHTML = counter.value().toString();
});
colorCountDivs[color].innerHTML = counter.value().toString();
}
Comment on lines +70 to +75
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Add cleanup for counter subscriptions.

The subscriptions to counter updates should be cleaned up to prevent memory leaks.

 function subscribeToCounterUpdates(color: Color, counter: LiveCounter) {
-  counter.subscribe(() => {
+  const subscription = counter.subscribe(() => {
     colorCountDivs[color].innerHTML = counter.value().toString();
   });
   colorCountDivs[color].innerHTML = counter.value().toString();
+  return subscription;  // Return subscription for cleanup
 }

And update the calling code to handle cleanup:

// Store subscriptions for cleanup
const subscriptions = new Map();

// In initCounters:
const subscription = subscribeToCounterUpdates(color, counter);
subscriptions.set(color, subscription);

// Add cleanup function:
function cleanup() {
  subscriptions.forEach(subscription => subscription.unsubscribe());
  subscriptions.clear();
}


function addEventListenersToButtons(root: LiveMap<DefaultRoot>) {
document.querySelectorAll('.vote-button').forEach((button) => {
const color = button.getAttribute('data-color') as Color;
button.addEventListener('click', () => {
root.get(color)?.increment(1);
});
});

countersReset.addEventListener('click', () => {
Object.values(Color).forEach(async (color) => root.set(color, await channel.objects.createCounter()));
});
}

main()
.then()
.catch((e) => console.error(e));
60 changes: 60 additions & 0 deletions examples/liveobjects-live-counter/javascript/src/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
:root {
--background: 0 0% 100%;
--foreground: 20 14.3% 4.1%;
--card: 0 0% 100%;
--card-foreground: 20 14.3% 4.1%;
--popover: 0 0% 100%;
--popover-foreground: 20 14.3% 4.1%;
--primary: 20 91% 54%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 209 100% 43%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 60 4.8% 95.9%;
--muted-foreground: 25 5.3% 44.7%;
--accent: 60 4.8% 95.9%;
--accent-foreground: 24 9.8% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 20 5.9% 90%;
--input: 20 5.9% 90%;
--ring: 24.6 95% 53.1%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
}

.dark {
--background: 20 14.3% 4.1%;
--foreground: 60 9.1% 97.8%;
--card: 20 14.3% 4.1%;
--card-foreground: 60 9.1% 97.8%;
--popover: 20 14.3% 4.1%;
--popover-foreground: 60 9.1% 97.8%;
--primary: 20 91% 54%;
--primary-foreground: 60 9.1% 97.8%;
--secondary: 209 100% 43%;
--secondary-foreground: 60 9.1% 97.8%;
--muted: 12 6.5% 15.1%;
--muted-foreground: 24 5.4% 63.9%;
--accent: 12 6.5% 15.1%;
--accent-foreground: 60 9.1% 97.8%;
--destructive: 0 72.2% 50.6%;
--destructive-foreground: 60 9.1% 97.8%;
--border: 12 6.5% 15.1%;
--input: 12 6.5% 15.1%;
--ring: 20.5 90.2% 48.2%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import baseConfig from '../../tailwind.config';
import type { Config } from 'tailwindcss';

const config: Config = {
...baseConfig,
content: ['./src/**/*.{js,ts,tsx}', './index.html'],
};

export default config;
5 changes: 5 additions & 0 deletions examples/liveobjects-live-counter/javascript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../tsconfig.json",
"include": ["./src/**/*.ts", "./src/**/*.tsx"],
"exclude": ["../../node_modules", "../../dist", "../../lib"]
}
8 changes: 8 additions & 0 deletions examples/liveobjects-live-counter/javascript/vite-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
interface ImportMetaEnv {
readonly VITE_ABLY_KEY: string;
// Add other environment variables here if needed
}

interface ImportMeta {
readonly env: ImportMetaEnv;
}
7 changes: 7 additions & 0 deletions examples/liveobjects-live-counter/javascript/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from 'vite';
import baseConfig from '../../vite.config';

export default defineConfig({
...baseConfig,
envDir: '../../',
});
Loading