Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.

Commit 8caa1c3

Browse files
live-cursors
1 parent 822818d commit 8caa1c3

File tree

9 files changed

+345
-226
lines changed

9 files changed

+345
-226
lines changed

examples/cursor/README.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
# Cursor Example
2+
3+
A real-time collaborative cursor demo built with ActorCore and Next.js. This example demonstrates how to create a multi-user application where users can see each other's cursor positions in real-time.
4+
5+
## Features
6+
7+
- Real-time cursor position tracking across multiple browser tabs/windows
8+
- Each user has a unique random ID, username, and color
9+
- Cursor list showing all connected users with their coordinates
10+
- Visual cursor pointers showing live cursor positions
11+
- Efficient cursor movement with lodash-es throttling (16ms)
12+
- Automatic cleanup when users disconnect
13+
- Built with React, Next.js, and Tailwind CSS
14+
15+
## Getting Started
16+
17+
1. Install dependencies:
18+
```bash
19+
yarn install
20+
```
21+
22+
2. Start both the development servers:
23+
```bash
24+
yarn dev
25+
```
26+
27+
This will start:
28+
- ActorCore server on port 6420
29+
- Next.js development server on port 3000
30+
31+
3. Open multiple browser tabs/windows to `http://localhost:3000` to see the cursors interact
32+
33+
## How It Works
34+
35+
The application uses ActorCore's real-time communication features to:
36+
37+
1. Create a unique cursor for each connected client with a random color and username
38+
2. Broadcast throttled cursor position updates (every 16ms) using lodash-es
39+
3. Maintain a list of all active cursors with their positions
40+
4. Remove cursors when clients disconnect
41+
42+
### Key Components
43+
44+
- `src/cursor-room.ts`: The ActorCore room that manages cursor state and communication
45+
- `src/components/CursorList.tsx`: React component that displays the list of connected cursors
46+
- `src/components/CursorPointers.tsx`: React component that renders the visual cursor indicators
47+
- `src/components/App.tsx`: Main application component that handles cursor movement and updates
48+
- `src/server.ts`: ActorCore server setup
49+
50+
## Architecture
51+
52+
The application follows a client-server architecture where:
53+
54+
1. The ActorCore server maintains the source of truth for cursor states
55+
2. Clients send cursor position updates when their mouse moves (throttled with lodash-es)
56+
3. The server broadcasts these updates to all other connected clients
57+
4. Each client renders both the cursor list and visual cursor pointers
58+
5. The UI is built with Next.js and styled with Tailwind CSS with a modern dark theme
59+
60+
## Development
61+
62+
To modify the example:
63+
64+
1. Edit `src/cursor-room.ts` to change cursor behavior or add new features
65+
2. Modify `src/components/CursorList.tsx` to update the cursor list visualization
66+
3. Modify `src/components/CursorPointers.tsx` to update the cursor pointer visualization
67+
4. Update `src/components/App.tsx` to change how cursor movements are tracked
68+
5. The server runs on port 6420 by default
69+
70+
## Dependencies
71+
72+
Key dependencies include:
73+
- `actor-core`: For real-time state management
74+
- `next`: For the React framework and development server
75+
- `lodash-es`: For efficient cursor movement throttling
76+
- `tailwindcss`: For styling
77+
78+
## License
79+
80+
This example is part of the ActorCore project and is available under the Apache 2.0 license.

examples/cursor/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scripts": {
77
"dev": "concurrently \"yarn dev:server\" \"yarn dev:client\"",
88
"dev:client": "next dev",
9-
"dev:server": "tsx src/server.ts",
9+
"dev:server": "tsx --watch src/server.ts",
1010
"build": "next build",
1111
"start": "next start",
1212
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
@@ -17,11 +17,13 @@
1717
"@actor-core/react": "workspace:*",
1818
"@actor-core/rivet": "workspace:*",
1919
"actor-core": "workspace:*",
20+
"lodash-es": "^4.17.21",
2021
"next": "^14.1.0",
2122
"react": "^18.2.0",
2223
"react-dom": "^18.2.0"
2324
},
2425
"devDependencies": {
26+
"@types/lodash-es": "^4.17.12",
2527
"@types/react": "^18.2.64",
2628
"@types/react-dom": "^18.2.21",
2729
"@typescript-eslint/eslint-plugin": "^7.1.1",

examples/cursor/src/App.tsx

Lines changed: 0 additions & 83 deletions
This file was deleted.
Lines changed: 102 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -1,102 +1,136 @@
11
import { useEffect, useState } from 'react';
22
import { createClient, type ActorHandle } from 'actor-core/client';
3-
import { type CursorRoom, type CursorState } from '../cursor-room';
4-
import CursorList from './CursorList';
53
import type { ActorCoreApp } from 'actor-core';
6-
7-
type App = ActorCoreApp<{
8-
"cursor-room": typeof CursorRoom;
9-
}>;
4+
import { throttle } from 'lodash-es';
5+
import CursorList from './CursorList';
6+
import CursorPointers from './CursorPointers';
7+
import type { App } from "../index";
8+
import { type cursorRoom, CursorState } from '../cursor-room';
109

1110
interface CursorEvent {
1211
id: string;
1312
cursor: CursorState;
1413
}
1514

15+
// Modern color palette
16+
const CURSOR_COLORS = [
17+
'#FF0080', // Pink
18+
'#7928CA', // Purple
19+
'#0070F3', // Blue
20+
'#00DFD8', // Cyan
21+
'#F5A623', // Orange
22+
'#79FFE1', // Mint
23+
'#F81CE5', // Magenta
24+
'#FF4D4D', // Red
25+
] as const;
26+
27+
// Constants
28+
const THROTTLE_MS = 16;
29+
const BACKGROUND_COLOR = '#06080C';
30+
1631
function App() {
17-
const [cursorRoom, setCursorRoom] = useState<ActorHandle<typeof CursorRoom> | null>(null);
18-
const [cursors, setCursors] = useState<CursorRoom['cursors']>({});
32+
const [room, setRoom] = useState<ActorHandle<typeof cursorRoom> | null>(null);
33+
const [cursors, setCursors] = useState<Record<string, CursorState>>({});
34+
const [userName] = useState(() => `User ${Math.floor(Math.random() * 10000)}`);
1935

2036
useEffect(() => {
21-
async function connect() {
37+
let mounted = true;
38+
let currentRoom: ActorHandle<typeof cursorRoom> | null = null;
39+
40+
const connect = async () => {
2241
try {
2342
const client = await createClient<App>("http://localhost:6420");
24-
const room = await client["cursor-room"].get();
25-
setCursorRoom(room);
26-
27-
// Subscribe to cursor events
28-
room.on("cursorMoved", (event: CursorEvent) => {
29-
setCursors(prev => ({
30-
...prev,
31-
[event.id]: event.cursor
32-
}));
33-
});
34-
35-
room.on("cursorAdded", (event: CursorEvent) => {
36-
setCursors(prev => ({
37-
...prev,
38-
[event.id]: event.cursor
39-
}));
40-
});
41-
42-
room.on("cursorRemoved", (id: string) => {
43-
setCursors(prev => {
44-
const next = { ...prev };
45-
delete next[id];
46-
return next;
43+
const newRoom = await client.cursorRoom.get();
44+
if (mounted) {
45+
currentRoom = newRoom;
46+
setRoom(newRoom);
47+
const randomColor = CURSOR_COLORS[Math.floor(Math.random() * CURSOR_COLORS.length)];
48+
49+
await Promise.all([
50+
newRoom.setName(userName),
51+
newRoom.setColor(randomColor)
52+
]);
53+
54+
// Subscribe to cursor events
55+
newRoom.on("cursorMoved", (event: CursorEvent) => {
56+
console.log('cursorMoved', event);
57+
setCursors(prev => ({ ...prev, [event.id]: event.cursor }));
58+
});
59+
60+
newRoom.on("cursorAdded", (event: CursorEvent) => {
61+
setCursors(prev => ({ ...prev, [event.id]: event.cursor }));
4762
});
48-
});
4963

50-
// Get initial cursors
51-
const initialCursors = await room.getCursors();
52-
setCursors(initialCursors);
53-
} catch (e) {
54-
console.error("Failed to connect:", e);
64+
newRoom.on("cursorRemoved", (id: string) => {
65+
setCursors(prev => {
66+
const next = { ...prev };
67+
delete next[id];
68+
return next;
69+
});
70+
});
71+
72+
// Get initial cursors
73+
const initialCursors = await newRoom.getCursors();
74+
setCursors(initialCursors);
75+
} else {
76+
// Component unmounted during connection, cleanup immediately
77+
await newRoom.dispose();
78+
}
79+
} catch (err) {
80+
console.error('Failed to connect:', err);
5581
}
56-
}
82+
};
83+
5784
connect();
85+
86+
return () => {
87+
mounted = false;
88+
room?.dispose();
89+
};
5890
}, []);
5991

6092
useEffect(() => {
61-
if (!cursorRoom) return;
93+
if (!room) return;
6294

6395
const throttledMouseMove = throttle((event: MouseEvent) => {
64-
cursorRoom.updateCursor(event.clientX, event.clientY);
65-
}, 50);
96+
room.updateCursor(event.clientX, event.clientY);
97+
}, THROTTLE_MS, { leading: true, trailing: true });
6698

6799
window.addEventListener('mousemove', throttledMouseMove);
68-
69100
return () => {
101+
throttledMouseMove.cancel(); // Properly cancel the throttled function
70102
window.removeEventListener('mousemove', throttledMouseMove);
71103
};
72-
}, [cursorRoom]);
104+
}, [room]);
73105

74106
return (
75-
<div className="min-h-screen bg-gray-100 p-8">
76-
<div className="max-w-4xl mx-auto">
77-
<h1 className="text-4xl font-bold mb-8">Live Cursors Demo</h1>
78-
<p className="text-lg mb-4">
79-
Move your cursor around to see it sync with other users in real-time.
80-
</p>
107+
<>
108+
<div
109+
className="min-h-screen text-white cursor-none"
110+
style={{
111+
backgroundColor: BACKGROUND_COLOR,
112+
backgroundImage: `radial-gradient(#ffffff1a 1.5px, transparent 1.5px)`,
113+
backgroundSize: '32px 32px'
114+
}}
115+
>
116+
<div className="relative">
117+
<div className="max-w-5xl mx-auto px-6 py-20">
118+
<h1 className="text-3xl mb-4 text-white flex items-center gap-3">
119+
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor">
120+
<path d="M3 3C3 3 17 9 17 9C17 9 12 11 12 11C12 11 9 17 9 17C9 17 3 3 3 3Z" />
121+
</svg>
122+
Live Cursor Demo
123+
</h1>
124+
<p className="text-base text-gray-400 mb-6 max-w-2xl">
125+
Built with the ActorCore framework. This demo showcases real-time state synchronization across multiple users with automatic persistence and low-latency updates.
126+
</p>
127+
</div>
128+
<CursorList cursors={cursors} />
129+
</div>
81130
</div>
82-
<CursorList cursors={cursors} />
83-
</div>
131+
<CursorPointers cursors={cursors} />
132+
</>
84133
);
85134
}
86135

87-
// Utility function to throttle function calls
88-
function throttle<T extends (...args: any[]) => void>(
89-
func: T,
90-
limit: number
91-
): (...args: Parameters<T>) => void {
92-
let inThrottle: boolean;
93-
return function(this: any, ...args: Parameters<T>) {
94-
if (!inThrottle) {
95-
func.apply(this, args);
96-
inThrottle = true;
97-
setTimeout(() => (inThrottle = false), limit);
98-
}
99-
};
100-
}
101-
102-
export default App;
136+
export default App;

0 commit comments

Comments
 (0)