Skip to content
This repository was archived by the owner on Oct 22, 2025. It is now read-only.
Closed
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
47 changes: 38 additions & 9 deletions docs/snippets/examples/document-js.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,32 @@ import { actor } from "actor-core";

export type Cursor = { x: number, y: number, userId: string };

export type CursorUpdateEvent = { userId: string, x: number, y: number };

export type TextUpdatedEvent = { text: string, userId: string };

export type UserDisconnectedEvent = { userId: string };

const document = actor({
state: {
text: "",
cursors: {} as Record<string, Cursor>,
},

onDisconnect: (c, conn) => {
console.log("onDisconnect(): " + conn.id);
delete c.state.cursors[conn.id];

// Broadcast removal
c.broadcastWithOptions(
{ exclude: [conn.id] },
"userDisconnected",
{
userId: conn.id
} as UserDisconnectedEvent
);
},

actions: {
getText: (c) => c.state.text,

Expand All @@ -18,25 +38,34 @@ const document = actor({
c.state.text = text;

// Broadcast update
c.broadcast("textUpdated", {
text,
userId: c.conn.id
});
c.broadcastWithOptions(
{ excludeSelf: true },
"textUpdated",
{
text,
userId: c.conn.id
} as TextUpdatedEvent
);
},

getCursors: (c) => c.state.cursors,

updateCursor: (c, x: number, y: number) => {
console.log("updateCursor(): " + c.conn.id);
// Update user location
const userId = c.conn.id;
c.state.cursors[userId] = { x, y, userId };

// Broadcast location
c.broadcast("cursorUpdated", {
userId,
x,
y
});
c.broadcastWithOptions(
{ excludeSelf: true },
"cursorUpdated",
{
userId,
x,
y
} as CursorUpdateEvent
);
},
}
});
Expand Down
84 changes: 54 additions & 30 deletions docs/snippets/examples/document-react.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,73 +2,97 @@
import { createClient } from "actor-core/client";
import { createReactActorCore } from "@actor-core/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type {
App, Cursor, TextUpdatedEvent,
CursorUpdateEvent, UserDisconnectedEvent
} from "../actors/app";

const client = createClient<App>("http://localhost:6420");
const { useActor, useActorEvent } = createReactActorCore(client);

export function DocumentEditor() {
// Connect to actor for this document ID from URL
const documentId = new URLSearchParams(window.location.search).get('id') || 'default-doc';
const [{ actor, connectionId }] = useActor("document", { tags: { id: documentId } });
const [{ actor, state }] = useActor("document", { tags: { id: documentId } });

// Local state
const [text, setText] = useState("");
const [cursorPos, setCursorPos] = useState({ x: 0, y: 0 });
const [otherCursors, setOtherCursors] = useState({});
const [otherCursors, setOtherCursors] = useState<Record<string, Cursor>>({});

// Load initial document state
useEffect(() => {
if (actor) {
if (actor && state === "created") {
actor.getText().then(setText);
actor.getCursors().then(setOtherCursors);
}
}, [actor]);
}, [actor, state]);

// Listen for updates from other users
useActorEvent({ actor, event: "textUpdated" }, ({ text: newText, userId: senderId }) => {
if (senderId !== connectionId) setText(newText);
useActorEvent({ actor, event: "textUpdated" }, (event) => {
const { text: newText, userId: _senderId } = event as TextUpdatedEvent;

setText(newText);
});

useActorEvent({ actor, event: "cursorUpdated" }, ({ userId: cursorUserId, x, y }) => {
if (cursorUserId !== connectionId) {
setOtherCursors(prev => ({
...prev,
[cursorUserId]: { x, y, userId: cursorUserId }
}));
}
useActorEvent({ actor, event: "cursorUpdated" }, (event) => {
const { userId: cursorUserId, x, y } = event as CursorUpdateEvent;

setOtherCursors(prev => ({
...prev,
[cursorUserId]: { x, y, userId: cursorUserId }
}));
});

// Update cursor position
const updateCursor = (e) => {
if (!actor) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;

useActorEvent({ actor, event: "userDisconnected" }, (event) => {
const { userId } = event as UserDisconnectedEvent;

if (x !== cursorPos.x || y !== cursorPos.y) {
setCursorPos({ x, y });
actor.updateCursor(x, y);
}
};
setOtherCursors(prev => {
const newCursors = { ...prev };
delete newCursors[userId];
return newCursors;
});
});


useEffect(() => {
if (!actor || state !== "created") return;

const updateCursor = ({ x, y }: { x: number, y: number }) => {

if (x !== cursorPos.x || y !== cursorPos.y) {
setCursorPos({ x, y });
actor.updateCursor(x, y);
}
};

window.addEventListener("mousemove", (e) => {
const x = e.clientX;
const y = e.clientY;

updateCursor({ x, y });
});
}, [actor, state]);

return (
<div className="document-editor">
<h2>Document: {documentId}</h2>

<div onMouseMove={updateCursor}>
<div>
<textarea
value={text}
onChange={(e) => {
const newText = e.target.value;
setText(newText);
actor?.setText(newText);
if (actor && state === "created") {
actor.setText(newText);
}
}}
placeholder="Start typing..."
/>

{/* Other users' cursors */}
{Object.values(otherCursors).map((cursor: any) => (
{Object.values(otherCursors).map((cursor) => (
<div
key={cursor.userId}
style={{
Expand Down
49 changes: 40 additions & 9 deletions docs/snippets/examples/document-sqlite.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,32 @@ import { documents, cursors } from "./schema";

export type Cursor = { x: number, y: number, userId: string };

export type CursorUpdateEvent = { userId: string, x: number, y: number };

export type TextUpdatedEvent = { text: string, userId: string };

export type UserDisconnectedEvent = { userId: string };

const document = actor({
sql: drizzle(),

onDisconnect: async (c, conn) => {
console.log("onDisconnect(): " + conn.id);
await c.db
.delete(documents)
.where({ userId: conn.id })
.execute();

// Broadcast removal
c.broadcastWithOptions(
{ exclude: [conn.id] },
"userDisconnected",
{
userId: conn.id
} as UserDisconnectedEvent
);
},

actions: {
getText: async (c) => {
const doc = await c.db
Expand All @@ -34,10 +57,14 @@ const document = actor({
});

// Broadcast update
c.broadcast("textUpdated", {
text,
userId: c.conn.id
});
c.broadcastWithOptions(
{ excludeSelf: true },
"textUpdated",
{
text,
userId: c.conn.id
} as TextUpdatedEvent
);
},

getCursors: async (c) => {
Expand Down Expand Up @@ -76,11 +103,15 @@ const document = actor({
});

// Broadcast location
c.broadcast("cursorUpdated", {
userId,
x,
y
});
c.broadcastWithOptions(
{ excludeSelf: true },
"cursorUpdated",
{
userId,
x,
y
} as CursorUpdateEvent
);
},
}
});
Expand Down
4 changes: 4 additions & 0 deletions examples/collab-document/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
.actorcore
node_modules
# React
build/
78 changes: 78 additions & 0 deletions examples/collab-document/actors/app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { actor, setup } from "actor-core";

export type Cursor = { x: number, y: number, userId: string };

export type CursorUpdateEvent = { userId: string, x: number, y: number };

export type TextUpdatedEvent = { text: string, userId: string };

export type UserDisconnectedEvent = { userId: string };

const document = actor({
state: {
text: "",
cursors: {} as Record<string, Cursor>,
},

onDisconnect: (c, conn) => {
console.log("onDisconnect(): " + conn.id);
delete c.state.cursors[conn.id];

// Broadcast removal
c.broadcastWithOptions(
{ exclude: [conn.id] },
"userDisconnected",
{
userId: conn.id
} as UserDisconnectedEvent
);
},

actions: {
getText: (c) => c.state.text,

// Update the document (real use case has better conflict resolution)
setText: (c, text: string) => {
// Save document state
c.state.text = text;

// Broadcast update
c.broadcastWithOptions(
{ excludeSelf: true },
"textUpdated",
{
text,
userId: c.conn.id
} as TextUpdatedEvent
);
},

getCursors: (c) => c.state.cursors,

updateCursor: (c, x: number, y: number) => {
console.log("updateCursor(): " + c.conn.id);
// Update user location
const userId = c.conn.id;
c.state.cursors[userId] = { x, y, userId };

// Broadcast location
c.broadcastWithOptions(
{ excludeSelf: true },
"cursorUpdated",
{
userId,
x,
y
} as CursorUpdateEvent
);
},
}
});

// Create and export the app
export const app = setup({
actors: { document }
});

// Export type for client type checking
export type App = typeof app;
Loading
Loading