Install one package, scale to production.
Just a library, no SaaS.
Long running tasks with state persistence, hibernation, and realtime
Replaces Durable Objects, Orleans, or Akka
Stay tuned for more
Build powerful applications with RivetKit’s libraries
import { actor } from "rivetkit";
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { getWeather } from "./my-utils";
export type Message = { role: "user" | "assistant"; content: string; timestamp: number; }
const aiAgent = actor({
// State is automatically persisted
state: {
messages: [] as Message[]
},
actions: {
// Get conversation history
getMessages: (c) => c.state.messages,
// Send a message to the AI and get a response
sendMessage: async (c, userMessage: string) => {
// Add user message to conversation
const userMsg: Message = {
role: "user",
content: userMessage,
timestamp: Date.now()
};
c.state.messages.push(userMsg);
// Generate AI response using Vercel AI SDK with tools
const { text } = await generateText({
model: openai("o3-mini"),
prompt: userMessage,
messages: c.state.messages,
tools: {
weather: tool({
description: 'Get the weather in a location',
parameters: {
location: {
type: 'string',
description: 'The location to get the weather for',
},
},
execute: async ({ location }) => {
return await getWeather(location);
},
}),
},
});
// Add AI response to conversation
const assistantMsg: Message = {
role: "assistant",
content: text,
timestamp: Date.now()
};
c.state.messages.push(assistantMsg);
// Broadcast the new message to all connected clients
c.broadcast("messageReceived", assistantMsg);
return assistantMsg;
},
}
});
export default aiAgent;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function AIAssistant() {
const [{ actor }] = useActor("aiAgent", { tags: { conversationId: "default" } });
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Load initial messages
useEffect(() => {
if (actor) {
actor.getMessages().then(setMessages);
}
}, [actor]);
// Listen for real-time messages
useActorEvent({ actor, event: "messageReceived" }, (message) => {
setMessages(prev => [...prev, message as Message]);
setIsLoading(false);
});
const handleSendMessage = async () => {
if (actor && input.trim()) {
setIsLoading(true);
// Add user message to UI immediately
const userMessage = { role: "user", content: input } as Message;
setMessages(prev => [...prev, userMessage]);
// Send to actor (AI response will come through the event)
await actor.sendMessage(input);
setInput("");
}
};
return (
<div className="ai-chat">
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">
Ask the AI assistant a question to get started
</div>
) : (
messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
<div className="avatar">
{msg.role === "user" ? "👤" : "🤖"}
</div>
<div className="content">{msg.content}</div>
</div>
))
)}
{isLoading && (
<div className="message assistant loading">
<div className="avatar">🤖</div>
<div className="content">Thinking...</div>
</div>
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && handleSendMessage()}
placeholder="Ask the AI assistant..."
disabled={isLoading}
/>
<button
onClick={handleSendMessage}
disabled={isLoading || !input.trim()}
>
Send
</button>
</div>
</div>
);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import { generateText, tool } from "ai";
import { openai } from "@ai-sdk/openai";
import { getWeather } from "./my-utils";
import { messages } from "./schema";
export type Message = { role: "user" | "assistant"; content: string; timestamp: number; }
const aiAgent = actor({
sql: drizzle(),
actions: {
// Get conversation history
getMessages: async (c) => {
const result = await c.db
.select()
.from(messages)
.orderBy(messages.timestamp.asc());
return result;
},
// Send a message to the AI and get a response
sendMessage: async (c, userMessage: string) => {
const now = Date.now();
// Add user message to conversation
const userMsg = {
conversationId: c.actorId, // Use the actor instance ID
role: "user",
content: userMessage,
};
// Store user message
await c.db
.insert(messages)
.values(userMsg);
// Get all messages
const allMessages = await c.db
.select()
.from(messages)
.orderBy(messages.timestamp.asc());
// Generate AI response using Vercel AI SDK with tools
const { text } = await generateText({
model: openai("o3-mini"),
prompt: userMessage,
messages: allMessages,
tools: {
weather: tool({
description: 'Get the weather in a location',
parameters: {
location: {
type: 'string',
description: 'The location to get the weather for',
},
},
execute: async ({ location }) => {
return await getWeather(location);
},
}),
},
});
// Add AI response to conversation
const assistantMsg = {
role: "assistant",
content: text,
};
// Store assistant message
await c.db
.insert(messages)
.values(assistantMsg);
// Broadcast the new message to all connected clients
c.broadcast("messageReceived", assistantMsg);
return assistantMsg;
},
}
});
export default aiAgent;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function AIAssistant() {
const [{ actor }] = useActor("aiAgent", { tags: { conversationId: "default" } });
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
// Load initial messages
useEffect(() => {
if (actor) {
actor.getMessages().then(setMessages);
}
}, [actor]);
// Listen for real-time messages
useActorEvent({ actor, event: "messageReceived" }, (message) => {
setMessages(prev => [...prev, message as Message]);
setIsLoading(false);
});
const handleSendMessage = async () => {
if (actor && input.trim()) {
setIsLoading(true);
// Add user message to UI immediately
const userMessage = { role: "user", content: input } as Message;
setMessages(prev => [...prev, userMessage]);
// Send to actor (AI response will come through the event)
await actor.sendMessage(input);
setInput("");
}
};
return (
<div className="ai-chat">
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">
Ask the AI assistant a question to get started
</div>
) : (
messages.map((msg, i) => (
<div key={i} className={`message ${msg.role}`}>
<div className="avatar">
{msg.role === "user" ? "👤" : "🤖"}
</div>
<div className="content">{msg.content}</div>
</div>
))
)}
{isLoading && (
<div className="message assistant loading">
<div className="avatar">🤖</div>
<div className="content">Thinking...</div>
</div>
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && handleSendMessage()}
placeholder="Ask the AI assistant..."
disabled={isLoading}
/>
<button
onClick={handleSendMessage}
disabled={isLoading || !input.trim()}
>
Send
</button>
</div>
</div>
);
}
import { actor } from "rivetkit";
import * as Y from 'yjs';
import { encodeStateAsUpdate, applyUpdate } from 'yjs';
const yjsDocument = actor({
// State: just the serialized Yjs document data
state: {
docData: "", // Base64 encoded Yjs document
lastModified: 0
},
// In-memory Yjs objects (not serialized)
createVars: () => ({
doc: new Y.Doc()
}),
// Initialize document from state when actor starts
onStart: (c) => {
if (c.state.docData) {
const binary = atob(c.state.docData);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
applyUpdate(c.vars.doc, bytes);
}
},
// Handle client connections
onConnect: (c) => {
// Send initial document state to client
const update = encodeStateAsUpdate(c.vars.doc);
const base64 = bufferToBase64(update);
c.conn.send("initialState", { update: base64 });
},
actions: {
// Apply a Yjs update from a client
applyUpdate: (c, updateBase64: string) => {
// Convert base64 to binary
const binary = atob(updateBase64);
const update = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
update[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(c.vars.doc, update);
// Save document state
const fullState = encodeStateAsUpdate(c.vars.doc);
c.state.docData = bufferToBase64(fullState);
c.state.lastModified = Date.now();
// Broadcast to all clients
c.broadcast("update", { update: updateBase64 });
}
}
});
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
export default yjsDocument;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect, useRef } from "react";
import * as Y from 'yjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function YjsEditor({ documentId = "shared-doc" }) {
// Connect to specific document using tags
const { actor } = useActor("yjsDocument", {
tags: { documentId }
});
// Document state
const [isLoading, setIsLoading] = useState(true);
const [text, setText] = useState("");
// Local Yjs document
const yDocRef = useRef<Y.Doc | null>(null);
// Flag to prevent infinite update loops
const updatingFromServer = useRef(false);
const updatingFromLocal = useRef(false);
// Track if we've initialized observation
const observationInitialized = useRef(false);
// Initialize local Yjs document and connect
useEffect(() => {
// Create Yjs document
const yDoc = new Y.Doc();
yDocRef.current = yDoc;
setIsLoading(false);
return () => {
// Clean up Yjs document
yDoc.destroy();
};
}, [actor]);
// Set up text observation
useEffect(() => {
const yDoc = yDocRef.current;
if (!yDoc || observationInitialized.current) return;
// Get the Yjs Text type from the document
const yText = yDoc.getText('content');
// Observe changes to the text
yText.observe(() => {
// Only update UI if change wasn't from server
if (!updatingFromServer.current) {
// Update React state
setText(yText.toString());
if (actor && !updatingFromLocal.current) {
// Set flag to prevent loops
updatingFromLocal.current = true;
// Convert update to base64 and send to server
const update = encodeStateAsUpdate(yDoc);
const base64 = bufferToBase64(update);
actor.applyUpdate(base64).finally(() => {
updatingFromLocal.current = false;
});
}
}
});
observationInitialized.current = true;
}, [actor]);
// Handle initial state from server
useActorEvent({ actor, event: "initialState" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying initial update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle updates from other clients
useActorEvent({ actor, event: "update" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle text changes from user
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!yDocRef.current) return;
const newText = e.target.value;
const yText = yDocRef.current.getText('content');
// Only update if text actually changed
if (newText !== yText.toString()) {
// Set flag to avoid loops
updatingFromLocal.current = true;
// Update Yjs document (this will trigger observe callback)
yDocRef.current.transact(() => {
yText.delete(0, yText.length);
yText.insert(0, newText);
});
updatingFromLocal.current = false;
}
};
if (isLoading) {
return <div className="loading">Loading collaborative document...</div>;
}
return (
<div className="yjs-editor">
<h3>Collaborative Document: {documentId}</h3>
<textarea
value={text}
onChange={handleTextChange}
placeholder="Start typing... All changes are synchronized in real-time!"
className="collaborative-textarea"
/>
</div>
);
}
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import * as Y from 'yjs';
import { encodeStateAsUpdate, applyUpdate } from 'yjs';
import { documents } from "./schema";
const yjsDocument = actor({
sql: drizzle(),
// In-memory Yjs objects (not serialized)
createVars: () => ({
doc: new Y.Doc()
}),
// Initialize document from state when actor starts
onStart: async (c) => {
// Get document data from database
const documentData = await c.db
.select()
.from(documents)
.get();
if (documentData?.docData) {
try {
// Parse the docData from string to binary
const binary = atob(documentData.docData);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
applyUpdate(c.vars.doc, bytes);
} catch (error) {
console.error("Failed to load document", error);
}
}
},
// Handle client connections
onConnect: (c) => {
// Send initial document state to client
const update = encodeStateAsUpdate(c.vars.doc);
const base64 = bufferToBase64(update);
c.conn.send("initialState", { update: base64 });
},
actions: {
// Apply a Yjs update from a client
applyUpdate: async (c, updateBase64: string) => {
try {
// Convert base64 to binary
const binary = atob(updateBase64);
const update = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
update[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(c.vars.doc, update);
// Save document state to database
const fullState = encodeStateAsUpdate(c.vars.doc);
const docData = bufferToBase64(fullState);
// Store in database
await c.db
.insert(documents)
.values({
docData
})
.onConflictDoUpdate({
target: documents.id,
set: {
docData
}
});
// Broadcast to all clients
c.broadcast("update", { update: updateBase64 });
} catch (error) {
console.error("Failed to apply update", error);
}
}
}
});
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
export default yjsDocument;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect, useRef } from "react";
import * as Y from 'yjs';
import { applyUpdate, encodeStateAsUpdate } from 'yjs';
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function YjsEditor({ documentId = "shared-doc" }) {
// Connect to specific document using tags
const { actor } = useActor("yjsDocument", {
tags: { documentId }
});
// Document state
const [isLoading, setIsLoading] = useState(true);
const [text, setText] = useState("");
// Local Yjs document
const yDocRef = useRef<Y.Doc | null>(null);
// Flag to prevent infinite update loops
const updatingFromServer = useRef(false);
const updatingFromLocal = useRef(false);
// Track if we've initialized observation
const observationInitialized = useRef(false);
// Initialize local Yjs document and connect
useEffect(() => {
// Create Yjs document
const yDoc = new Y.Doc();
yDocRef.current = yDoc;
setIsLoading(false);
return () => {
// Clean up Yjs document
yDoc.destroy();
};
}, [actor]);
// Set up text observation
useEffect(() => {
const yDoc = yDocRef.current;
if (!yDoc || observationInitialized.current) return;
// Get the Yjs Text type from the document
const yText = yDoc.getText('content');
// Observe changes to the text
yText.observe(() => {
// Only update UI if change wasn't from server
if (!updatingFromServer.current) {
// Update React state
setText(yText.toString());
if (actor && !updatingFromLocal.current) {
// Set flag to prevent loops
updatingFromLocal.current = true;
// Convert update to base64 and send to server
const update = encodeStateAsUpdate(yDoc);
const base64 = bufferToBase64(update);
actor.applyUpdate(base64).finally(() => {
updatingFromLocal.current = false;
});
}
}
});
observationInitialized.current = true;
}, [actor]);
// Handle initial state from server
useActorEvent({ actor, event: "initialState" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying initial update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle updates from other clients
useActorEvent({ actor, event: "update" }, ({ update }) => {
const yDoc = yDocRef.current;
if (!yDoc) return;
// Set flag to prevent update loops
updatingFromServer.current = true;
try {
// Convert base64 to binary and apply to document
const binary = atob(update);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
// Apply update to Yjs document
applyUpdate(yDoc, bytes);
// Update React state
const yText = yDoc.getText('content');
setText(yText.toString());
} catch (error) {
console.error("Error applying update:", error);
} finally {
updatingFromServer.current = false;
}
});
// Handle text changes from user
const handleTextChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
if (!yDocRef.current) return;
const newText = e.target.value;
const yText = yDocRef.current.getText('content');
// Only update if text actually changed
if (newText !== yText.toString()) {
// Set flag to avoid loops
updatingFromLocal.current = true;
// Update Yjs document (this will trigger observe callback)
yDocRef.current.transact(() => {
yText.delete(0, yText.length);
yText.insert(0, newText);
});
updatingFromLocal.current = false;
}
};
if (isLoading) {
return <div className="loading">Loading collaborative document...</div>;
}
return (
<div className="yjs-editor">
<h3>Collaborative Document: {documentId}</h3>
<textarea
value={text}
onChange={handleTextChange}
placeholder="Start typing... All changes are synchronized in real-time!"
className="collaborative-textarea"
/>
</div>
);
}
// Helper to convert ArrayBuffer to base64
function bufferToBase64(buffer: Uint8Array): string {
let binary = '';
for (let i = 0; i < buffer.byteLength; i++) {
binary += String.fromCharCode(buffer[i]);
}
return btoa(binary);
}
import { actor } from "rivetkit";
export type Message = { sender: string; text: string; timestamp: number; }
const chatRoom = actor({
// State is automatically persisted
state: {
messages: [] as Message[]
},
actions: {
sendMessage: (c, sender: string, text: string) => {
const message = { sender, text, timestamp: Date.now() };
// Any changes to state are automatically saved
c.state.messages.push(message);
// Broadcast events trigger real-time updates in connected clients
c.broadcast("newMessage", message);
},
getHistory: (c) => c.state.messages
}
});
export default chatRoom;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function ChatRoom({ roomId = "general" }) {
// Connect to specific chat room using tags
const [{ actor }] = useActor("chatRoom", {
tags: { roomId }
});
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
// Load initial state
useEffect(() => {
if (actor) {
// Load chat history
actor.getHistory().then(setMessages);
}
}, [actor]);
// Listen for real-time updates from the server
useActorEvent({ actor, event: "newMessage" }, (message) => {
setMessages(prev => [...prev, message]);
});
const sendMessage = () => {
if (actor && input.trim()) {
actor.sendMessage("User", input);
setInput("");
}
};
return (
<div className="chat-container">
<div className="room-header">
<h3>Chat Room: {roomId}</h3>
</div>
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">No messages yet. Start the conversation!</div>
) : (
messages.map((msg, i) => (
<div key={i} className="message">
<b>{msg.sender}:</b> {msg.text}
<span className="timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
))
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import { messages } from "./schema";
export type Message = { sender: string; text: string; timestamp: number; }
const chatRoom = actor({
sql: drizzle(),
actions: {
sendMessage: async (c, sender: string, text: string) => {
const message = {
sender,
text,
timestamp: Date.now(),
};
// Insert the message into SQLite
await c.db.insert(messages).values(message);
// Broadcast to all connected clients
c.broadcast("newMessage", message);
// Return the created message (matches JS memory version)
return message;
},
getHistory: async (c) => {
// Query all messages ordered by timestamp
const result = await c.db
.select()
.from(messages)
.orderBy(messages.timestamp);
return result as Message[];
}
}
});
export default chatRoom;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { Message } from "./actor";
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function ChatRoom({ roomId = "general" }) {
// Connect to specific chat room using tags
const [{ actor }] = useActor("chatRoom", {
tags: { roomId }
});
const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
// Load initial state
useEffect(() => {
if (actor) {
// Load chat history
actor.getHistory().then(setMessages);
}
}, [actor]);
// Listen for real-time updates from the server
useActorEvent({ actor, event: "newMessage" }, (message) => {
setMessages(prev => [...prev, message]);
});
const sendMessage = () => {
if (actor && input.trim()) {
actor.sendMessage("User", input);
setInput("");
}
};
return (
<div className="chat-container">
<div className="room-header">
<h3>Chat Room: {roomId}</h3>
</div>
<div className="messages">
{messages.length === 0 ? (
<div className="empty-message">No messages yet. Start the conversation!</div>
) : (
messages.map((msg, i) => (
<div key={i} className="message">
<b>{msg.sender}:</b> {msg.text}
<span className="timestamp">
{new Date(msg.timestamp).toLocaleTimeString()}
</span>
</div>
))
)}
</div>
<div className="input-area">
<input
value={input}
onChange={e => setInput(e.target.value)}
onKeyPress={e => e.key === "Enter" && sendMessage()}
placeholder="Type a message..."
/>
<button onClick={sendMessage}>Send</button>
</div>
</div>
);
}
import { actor } from "rivetkit";
import { authenticate } from "./my-utils";
export type Note = { id: string; content: string; updatedAt: number };
// User notes actor
const notes = actor({
state: {
notes: [] as Note[]
},
// Authenticate
createConnState: async (c, { params }) => {
const token = params.token;
const userId = await authenticate(token);
return { userId };
},
actions: {
// Get all notes
getNotes: (c) => c.state.notes,
// Update note or create if it doesn't exist
updateNote: (c, { id, content }) => {
const noteIndex = c.state.notes.findIndex(note => note.id === id);
let note;
if (noteIndex >= 0) {
// Update existing note
note = c.state.notes[noteIndex];
note.content = content;
note.updatedAt = Date.now();
c.broadcast("noteUpdated", note);
} else {
// Create new note
note = {
id: id || `note-${Date.now()}`,
content,
updatedAt: Date.now()
};
c.state.notes.push(note);
c.broadcast("noteAdded", note);
}
return note;
},
// Delete note
deleteNote: (c, { id }) => {
const noteIndex = c.state.notes.findIndex(note => note.id === id);
if (noteIndex >= 0) {
c.state.notes.splice(noteIndex, 1);
c.broadcast("noteDeleted", { id });
}
}
}
});
export default notes;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
const client = createClient("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function NotesApp({ userId }: { userId: string }) {
const [notes, setNotes] = useState<Array<{ id: string, content: string }>>([]);
const [newNote, setNewNote] = useState("");
// Connect to actor with auth token
const [{ actor }] = useActor("notes", {
params: { userId, token: "demo-token" }
});
// Load initial notes
useEffect(() => {
if (actor) {
actor.getNotes().then(setNotes);
}
}, [actor]);
// Add a new note
const addNote = async () => {
if (actor && newNote.trim()) {
await actor.updateNote({ id: `note-${Date.now()}`, content: newNote });
setNewNote("");
}
};
// Delete a note
const deleteNote = (id: string) => {
if (actor) {
actor.deleteNote({ id });
}
};
// Listen for realtime updates
useActorEvent({ actor, event: "noteAdded" }, (note) => {
setNotes(notes => [...notes, note]);
});
useActorEvent({ actor, event: "noteUpdated" }, (updatedNote) => {
setNotes(notes => notes.map(note =>
note.id === updatedNote.id ? updatedNote : note
));
});
useActorEvent({ actor, event: "noteDeleted" }, ({ id }) => {
setNotes(notes => notes.filter(note => note.id !== id));
});
return (
<div>
<h2>My Notes</h2>
<div>
<input
value={newNote}
onChange={e => setNewNote(e.target.value)}
placeholder="Enter a new note"
/>
<button onClick={addNote}>Add</button>
</div>
<ul>
{notes.map(note => (
<li key={note.id}>
{note.content}
<button onClick={() => deleteNote(note.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import { notes } from "./schema";
import { authenticate } from "./my-utils";
export type Note = { id: string; content: string; updatedAt: number };
// User notes actor
const userNotes = actor({
sql: drizzle(),
// Authenticate
createConnState: async (c, { params }) => {
const token = params.token;
const userId = await authenticate(token);
return { userId };
},
actions: {
// Get all notes
getNotes: async (c) => {
const result = await c.db
.select()
.from(notes);
return result;
},
// Update note or create if it doesn't exist
updateNote: async (c, { id, content }) => {
// Ensure the note ID exists or create a new one
const noteId = id || `note-${Date.now()}`;
// Check if note exists
const existingNote = await c.db
.select()
.from(notes)
.where(notes.id.equals(noteId))
.get();
if (existingNote) {
// Update existing note
await c.db
.update(notes)
.set({
content
})
.where(notes.id.equals(noteId));
const updatedNote = {
id: noteId,
content
};
c.broadcast("noteUpdated", updatedNote);
return updatedNote;
} else {
// Create new note
const newNote = {
id: noteId,
content
};
await c.db
.insert(notes)
.values(newNote);
c.broadcast("noteAdded", newNote);
return newNote;
}
},
// Delete note
deleteNote: async (c, { id }) => {
// Delete the note
await c.db
.delete(notes)
.where(notes.id.equals(id));
c.broadcast("noteDeleted", { id });
}
}
});
export default userNotes;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
const client = createClient("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function NotesApp({ userId }: { userId: string }) {
const [notes, setNotes] = useState<Array<{ id: string, content: string }>>([]);
const [newNote, setNewNote] = useState("");
// Connect to actor with auth token
const [{ actor }] = useActor("notes", {
params: { userId, token: "demo-token" }
});
// Load initial notes
useEffect(() => {
if (actor) {
actor.getNotes().then(setNotes);
}
}, [actor]);
// Add a new note
const addNote = async () => {
if (actor && newNote.trim()) {
await actor.updateNote({ id: `note-${Date.now()}`, content: newNote });
setNewNote("");
}
};
// Delete a note
const deleteNote = (id: string) => {
if (actor) {
actor.deleteNote({ id });
}
};
// Listen for realtime updates
useActorEvent({ actor, event: "noteAdded" }, (note) => {
setNotes(notes => [...notes, note]);
});
useActorEvent({ actor, event: "noteUpdated" }, (updatedNote) => {
setNotes(notes => notes.map(note =>
note.id === updatedNote.id ? updatedNote : note
));
});
useActorEvent({ actor, event: "noteDeleted" }, ({ id }) => {
setNotes(notes => notes.filter(note => note.id !== id));
});
return (
<div>
<h2>My Notes</h2>
<div>
<input
value={newNote}
onChange={e => setNewNote(e.target.value)}
placeholder="Enter a new note"
/>
<button onClick={addNote}>Add</button>
</div>
<ul>
{notes.map(note => (
<li key={note.id}>
{note.content}
<button onClick={() => deleteNote(note.id)}>Delete</button>
</li>
))}
</ul>
</div>
);
}
import { actor } from "rivetkit";
// Simple rate limiter - allows 5 requests per minute
const rateLimiter = actor({
state: {
count: 0,
resetAt: 0
},
actions: {
// Check if request is allowed
checkLimit: (c) => {
const now = Date.now();
// Reset if expired
if (now > c.state.resetAt) {
c.state.count = 0;
c.state.resetAt = now + 60000; // 1 minute window
}
// Check if under limit
const allowed = c.state.count < 5;
// Increment if allowed
if (allowed) {
c.state.count++;
}
return {
allowed,
remaining: 5 - c.state.count,
resetsIn: Math.round((c.state.resetAt - now) / 1000)
};
}
}
});
export default rateLimiter;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState } from "react";
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:8080");
const { useActor } = createReactRivetKit(client);
export function RateLimiter() {
// Connect to API rate limiter for user-123
const [{ actor }] = useActor("rateLimiter", { tags: { userId: "user-123" } });
const [result, setResult] = useState<{
allowed: boolean;
remaining: number;
resetsIn: number;
} | null>(null);
// Make a request
const makeRequest = async () => {
if (!actor) return;
const response = await actor.checkLimit();
setResult(response);
};
return (
<div>
<h2>Rate Limiter (5 req/min)</h2>
<button onClick={makeRequest}>Make Request</button>
{result && (
<div>
<p>Status: {result.allowed ? "Allowed" : "Blocked"}</p>
<p>Remaining: {result.remaining}</p>
<p>Resets in: {result.resetsIn} seconds</p>
</div>
)}
</div>
);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import { limiters } from "./schema";
// Simple rate limiter - allows 5 requests per minute
const rateLimiter = actor({
sql: drizzle(),
actions: {
// Check if request is allowed
checkLimit: async (c) => {
const now = Date.now();
// Get the current limiter state from database
const limiterState = await c.db
.select()
.from(limiters)
.get();
// If no record exists, create one
if (!limiterState) {
await c.db.insert(limiters).values({
count: 1,
resetAt: now + 60000 // 1 minute window
});
return {
allowed: true,
remaining: 4,
resetsIn: 60
};
}
// Reset if expired
if (now > limiterState.resetAt) {
await c.db.update(limiters)
.set({
count: 1,
resetAt: now + 60000 // 1 minute window
});
return {
allowed: true,
remaining: 4,
resetsIn: 60
};
}
// Check if under limit
const allowed = limiterState.count < 5;
// Increment if allowed
if (allowed) {
await c.db.update(limiters)
.set({ count: limiterState.count + 1 });
}
return {
allowed,
remaining: 5 - (allowed ? limiterState.count + 1 : limiterState.count),
resetsIn: Math.round((limiterState.resetAt - now) / 1000)
};
}
}
});
export default rateLimiter;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState } from "react";
import type { App } from "../actors/app";
const client = createClient<App>("http://localhost:8080");
const { useActor } = createReactRivetKit(client);
export function RateLimiter() {
// Connect to API rate limiter for user-123
const [{ actor }] = useActor("rateLimiter", { tags: { userId: "user-123" } });
const [result, setResult] = useState<{
allowed: boolean;
remaining: number;
resetsIn: number;
} | null>(null);
// Make a request
const makeRequest = async () => {
if (!actor) return;
const response = await actor.checkLimit();
setResult(response);
};
return (
<div>
<h2>Rate Limiter (5 req/min)</h2>
<button onClick={makeRequest}>Make Request</button>
{result && (
<div>
<p>Status: {result.allowed ? "Allowed" : "Blocked"}</p>
<p>Remaining: {result.remaining}</p>
<p>Resets in: {result.resetsIn} seconds</p>
</div>
)}
</div>
);
}
import { actor } from "rivetkit";
export type StreamState = {
topValues: number[];
};
// Simple top-K stream processor example
const streamProcessor = actor({
state: {
topValues: [] as number[]
},
actions: {
getTopValues: (c) => c.state.topValues,
// Add value and keep top 3
addValue: (c, value: number) => {
// Insert new value if needed
const insertAt = c.state.topValues.findIndex(v => value > v);
if (insertAt === -1) {
c.state.topValues.push(value);
} else {
c.state.topValues.splice(insertAt, 0, value);
}
// Keep only top 3
if (c.state.topValues.length > 3) {
c.state.topValues.length = 3;
}
// Broadcast update to all clients
c.broadcast("updated", { topValues: c.state.topValues });
return c.state.topValues;
},
}
});
export default streamProcessor;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { StreamState } from "./actor"; // Import shared types from actor
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function StreamExample() {
const [{ actor }] = useActor("streamProcessor");
const [topValues, setTopValues] = useState<number[]>([]);
const [newValue, setNewValue] = useState<number>(0);
// Load initial values
useEffect(() => {
if (actor) {
actor.getTopValues().then(setTopValues);
}
}, [actor]);
// Listen for updates from other clients
useActorEvent({ actor, event: "updated" }, ({ topValues }) => {
setTopValues(topValues);
});
// Add a new value to the stream
const handleAddValue = () => {
if (actor) {
actor.addValue(newValue).then(setTopValues);
setNewValue(0);
}
};
return (
<div>
<h2>Top 3 Values</h2>
<ul>
{topValues.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
<input
type="number"
value={newValue}
onChange={(e) => setNewValue(Number(e.target.value))}
/>
<button onClick={handleAddValue}>Add Value</button>
</div>
);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import { streams, streamValues } from "./schema";
export type StreamState = { topValues: number[]; };
// Simple top-K stream processor example
const streamProcessor = actor({
sql: drizzle(),
actions: {
getTopValues: async (c) => {
// Get the top 3 values sorted in descending order
const result = await c.db
.select()
.from(streamValues)
.orderBy(streamValues.value.desc())
.limit(3);
return result.map(r => r.value);
},
// Add value and keep top 3
addValue: async (c, value: number) => {
// Insert the new value
await c.db
.insert(streamValues)
.values({
value
});
// Get the updated top 3 values
const topValues = await c.db
.select()
.from(streamValues)
.orderBy(streamValues.value.desc())
.limit(3);
// Delete values that are no longer in the top 3
if (topValues.length === 3) {
await c.db
.delete(streamValues)
.where(streamValues.value.lt(topValues[2].value));
}
const topValuesArray = topValues.map(r => r.value);
// Broadcast update to all clients
c.broadcast("updated", { topValues: topValuesArray });
return topValuesArray;
},
}
});
export default streamProcessor;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect } from "react";
import type { App } from "../actors/app";
import type { StreamState } from "./actor"; // Import shared types from actor
const client = createClient<App>("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function StreamExample() {
const [{ actor }] = useActor("streamProcessor");
const [topValues, setTopValues] = useState<number[]>([]);
const [newValue, setNewValue] = useState<number>(0);
// Load initial values
useEffect(() => {
if (actor) {
actor.getTopValues().then(setTopValues);
}
}, [actor]);
// Listen for updates from other clients
useActorEvent({ actor, event: "updated" }, ({ topValues }) => {
setTopValues(topValues);
});
// Add a new value to the stream
const handleAddValue = () => {
if (actor) {
actor.addValue(newValue).then(setTopValues);
setNewValue(0);
}
};
return (
<div>
<h2>Top 3 Values</h2>
<ul>
{topValues.map((value, i) => (
<li key={i}>{value}</li>
))}
</ul>
<input
type="number"
value={newValue}
onChange={(e) => setNewValue(Number(e.target.value))}
/>
<button onClick={handleAddValue}>Add Value</button>
</div>
);
}
import { actor } from "rivetkit";
export type Position = { x: number; y: number };
export type Input = { x: number; y: number };
export type Player = { id: string; position: Position; input: Input };
const gameRoom = actor({
state: {
players: {} as Record<string, Player>,
mapSize: 800
},
onStart: (c) => {
// Set up game update loop
setInterval(() => {
const worldUpdate = { playerList: [] };
for (const id in c.state.players) {
const player = c.state.players[id];
const speed = 5;
// Update position based on input
player.position.x += player.input.x * speed;
player.position.y += player.input.y * speed;
// Keep player in bounds
player.position.x = Math.max(0, Math.min(player.position.x, c.state.mapSize));
player.position.y = Math.max(0, Math.min(player.position.y, c.state.mapSize));
// Add to list for broadcast
worldUpdate.playerList.push(player);
}
// Broadcast world state
c.broadcast("worldUpdate", worldUpdate);
}, 50);
},
// Add player to game
onConnect: (c) => {
const id = c.conn.id;
c.state.players[id] = {
id,
position: {
x: Math.floor(Math.random() * c.state.mapSize),
y: Math.floor(Math.random() * c.state.mapSize)
},
input: { x: 0, y: 0 }
};
},
// Remove player from game
onDisconnect: (c) => {
delete c.state.players[c.conn.id];
},
actions: {
// Update movement
setInput: (c, input: Input) => {
const player = c.state.players[c.conn.id];
if (player) player.input = input;
}
}
});
export default gameRoom;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect, useRef } from "react";
import type { Player } from "./actor";
const client = createClient("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function MultiplayerGame() {
const [{ actor, connectionId }] = useActor("gameRoom");
const [players, setPlayers] = useState<Player[]>([]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const keysPressed = useRef<Record<string, boolean>>({});
// Set up game
useEffect(() => {
if (!actor) return;
// Set up keyboard handlers
const handleKeyDown = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = true;
};
const handleKeyUp = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = false;
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
// Input update loop
const inputInterval = setInterval(() => {
const input = { x: 0, y: 0 };
if (keysPressed.current["w"] || keysPressed.current["arrowup"]) input.y = -1;
if (keysPressed.current["s"] || keysPressed.current["arrowdown"]) input.y = 1;
if (keysPressed.current["a"] || keysPressed.current["arrowleft"]) input.x = -1;
if (keysPressed.current["d"] || keysPressed.current["arrowright"]) input.x = 1;
actor.setInput(input);
}, 50);
// Rendering loop
const renderLoop = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Use for loop instead of forEach
for (let i = 0; i < players.length; i++) {
const player = players[i];
ctx.fillStyle = player.id === connectionId ? "blue" : "gray";
ctx.beginPath();
ctx.arc(player.position.x, player.position.y, 10, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(renderLoop);
};
const animationId = requestAnimationFrame(renderLoop);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
clearInterval(inputInterval);
cancelAnimationFrame(animationId);
};
}, [actor, connectionId, players]);
// Listen for world updates
useActorEvent({ actor, event: "worldUpdate" }, ({ players: updatedPlayers }) => {
setPlayers(updatedPlayers);
});
return (
<div>
<canvas
ref={canvasRef}
width={800}
height={600}
style={{ border: "1px solid black" }}
/>
<p>Move: WASD or Arrow Keys</p>
</div>
);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import { players, gameSettings } from "./schema";
export type Position = { x: number; y: number };
export type Input = { x: number; y: number };
export type Player = { id: string; position: Position; input: Input };
const gameRoom = actor({
sql: drizzle(),
// Store game settings and player inputs in memory for performance
createVars: () => ({
playerCache: {} as Record<string, Player>,
mapSize: 800
}),
onStart: async (c) => {
// Get or initialize game settings
const settings = await c.db
.select()
.from(gameSettings)
.get();
if (settings) {
c.vars.mapSize = settings.mapSize;
} else {
await c.db
.insert(gameSettings)
.values({
mapSize: c.vars.mapSize
});
}
// Load existing players into memory
const existingPlayers = await c.db
.select()
.from(players);
for (const player of existingPlayers) {
c.vars.playerCache[player.id] = {
id: player.id,
position: {
x: player.positionX,
y: player.positionY
},
input: {
x: player.inputX,
y: player.inputY
}
};
}
// Set up game update loop
setInterval(async () => {
const worldUpdate = { playerList: [] };
let changed = false;
for (const id in c.vars.playerCache) {
const player = c.vars.playerCache[id];
const speed = 5;
// Update position based on input
player.position.x += player.input.x * speed;
player.position.y += player.input.y * speed;
// Keep player in bounds
player.position.x = Math.max(0, Math.min(player.position.x, c.vars.mapSize));
player.position.y = Math.max(0, Math.min(player.position.y, c.vars.mapSize));
// Add to list for broadcast
worldUpdate.playerList.push(player);
changed = true;
}
// Save player positions to database if changed
if (changed) {
for (const id in c.vars.playerCache) {
const player = c.vars.playerCache[id];
await c.db
.update(players)
.set({
positionX: player.position.x,
positionY: player.position.y
})
.where(players.id.equals(id));
}
// Broadcast world state
c.broadcast("worldUpdate", worldUpdate);
}
}, 50);
},
// Add player to game
onConnect: async (c) => {
const id = c.conn.id;
const randomX = Math.floor(Math.random() * c.vars.mapSize);
const randomY = Math.floor(Math.random() * c.vars.mapSize);
// Create player in memory cache
c.vars.playerCache[id] = {
id,
position: {
x: randomX,
y: randomY
},
input: { x: 0, y: 0 }
};
// Save player to database
await c.db
.insert(players)
.values({
id,
positionX: randomX,
positionY: randomY,
inputX: 0,
inputY: 0
})
.onConflictDoUpdate({
target: players.id,
set: {
positionX: randomX,
positionY: randomY,
inputX: 0,
inputY: 0
}
});
},
// Remove player from game
onDisconnect: async (c) => {
const id = c.conn.id;
// Remove from memory cache
delete c.vars.playerCache[id];
// Remove from database
await c.db
.delete(players)
.where(players.id.equals(id));
},
actions: {
// Update movement
setInput: async (c, input: Input) => {
const id = c.conn.id;
const player = c.vars.playerCache[id];
if (player) {
// Update in memory for fast response
player.input = input;
// Update in database
await c.db
.update(players)
.set({
inputX: input.x,
inputY: input.y
})
.where(players.id.equals(id));
}
}
}
});
export default gameRoom;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect, useRef } from "react";
import type { Player } from "./actor";
const client = createClient("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function MultiplayerGame() {
const [{ actor, connectionId }] = useActor("gameRoom");
const [players, setPlayers] = useState<Player[]>([]);
const canvasRef = useRef<HTMLCanvasElement>(null);
const keysPressed = useRef<Record<string, boolean>>({});
// Set up game
useEffect(() => {
if (!actor) return;
// Set up keyboard handlers
const handleKeyDown = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = true;
};
const handleKeyUp = (e: KeyboardEvent) => {
keysPressed.current[e.key.toLowerCase()] = false;
};
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
// Input update loop
const inputInterval = setInterval(() => {
const input = { x: 0, y: 0 };
if (keysPressed.current["w"] || keysPressed.current["arrowup"]) input.y = -1;
if (keysPressed.current["s"] || keysPressed.current["arrowdown"]) input.y = 1;
if (keysPressed.current["a"] || keysPressed.current["arrowleft"]) input.x = -1;
if (keysPressed.current["d"] || keysPressed.current["arrowright"]) input.x = 1;
actor.setInput(input);
}, 50);
// Rendering loop
const renderLoop = () => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Use for loop instead of forEach
for (let i = 0; i < players.length; i++) {
const player = players[i];
ctx.fillStyle = player.id === connectionId ? "blue" : "gray";
ctx.beginPath();
ctx.arc(player.position.x, player.position.y, 10, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(renderLoop);
};
const animationId = requestAnimationFrame(renderLoop);
return () => {
window.removeEventListener("keydown", handleKeyDown);
window.removeEventListener("keyup", handleKeyUp);
clearInterval(inputInterval);
cancelAnimationFrame(animationId);
};
}, [actor, connectionId, players]);
// Listen for world updates
useActorEvent({ actor, event: "worldUpdate" }, ({ players: updatedPlayers }) => {
setPlayers(updatedPlayers);
});
return (
<div>
<canvas
ref={canvasRef}
width={800}
height={600}
style={{ border: "1px solid black" }}
/>
<p>Move: WASD or Arrow Keys</p>
</div>
);
}
import { actor } from "rivetkit";
export type Contact = { id: string; name: string; email: string; phone: string; updatedAt: number; }
const contacts = actor({
// State is automatically persisted
state: {
contacts: {}
},
actions: {
// Gets changes after the last timestamp (when coming back online)
getChanges: (c, after: number = 0) => {
const changes = Object.values(c.state.contacts)
.filter(contact => contact.updatedAt > after);
return {
changes,
timestamp: Date.now()
};
},
// Pushes new changes from the client & handles conflicts
pushChanges: (c, contacts: Contact[]) => {
let changed = false;
contacts.forEach(contact => {
const existing = c.state.contacts[contact.id];
if (!existing || existing.updatedAt < contact.updatedAt) {
c.state.contacts[contact.id] = contact;
changed = true;
}
});
if (changed) {
c.broadcast("contactsChanged", {
contacts: Object.values(c.state.contacts)
});
}
return { timestamp: Date.now() };
}
}
});
export default contacts;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect, useRef } from "react";
import type { Contact } from "./actor";
const client = createClient("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function ContactsApp() {
const { actor } = useActor("contacts");
const [contacts, setContacts] = useState<Contact[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [syncStatus, setSyncStatus] = useState("Idle");
const lastSyncTime = useRef(0);
// Load initial contacts
useEffect(() => {
if (!actor) return;
actor.getChanges(0).then(data => {
setContacts(data.changes);
lastSyncTime.current = data.timestamp;
setSyncStatus("Synced");
});
}, [actor]);
// Handle contact events
useActorEvent({ actor, event: "contactsChanged" }, ({ contacts: updatedContacts }) => {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
updatedContacts.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
});
// Sync periodically
useEffect(() => {
if (!actor) return;
const sync = async () => {
setSyncStatus("Syncing...");
try {
// Get remote changes
const changes = await actor.getChanges(lastSyncTime.current);
// Apply remote changes
if (changes.changes.length > 0) {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
changes.changes.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
}
// Push local changes
const localChanges = contacts.filter(c => c.updatedAt > lastSyncTime.current);
if (localChanges.length > 0) {
await actor.pushChanges(localChanges);
}
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
const intervalId = setInterval(sync, 5000);
return () => clearInterval(intervalId);
}, [actor, contacts]);
// Add new contact (local first)
const addContact = () => {
if (!name.trim()) return;
const newContact: Contact = {
id: Date.now().toString(),
name,
email,
phone,
updatedAt: Date.now()
};
setContacts(prev => [...prev, newContact]);
if (actor) {
actor.pushChanges([newContact]);
}
setName("");
setEmail("");
setPhone("");
};
// Delete contact (implemented as update with empty name)
const deleteContact = (id: string) => {
setContacts(prev => {
const updatedContacts = prev.map(c =>
c.id === id
? { ...c, name: "", updatedAt: Date.now() }
: c
);
if (actor) {
const deleted = updatedContacts.find(c => c.id === id);
if (deleted) {
actor.pushChanges([deleted]);
}
}
return updatedContacts.filter(c => c.name !== "");
});
};
// Manual sync
const handleSync = async () => {
if (!actor) return;
setSyncStatus("Syncing...");
try {
// Push all contacts
await actor.pushChanges(contacts);
// Get all changes
const changes = await actor.getChanges(0);
setContacts(changes.changes);
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
return (
<div className="contacts-app">
<div className="contacts-header">
<h2>Contacts</h2>
<div className="sync-status">
<span>{syncStatus}</span>
<button onClick={handleSync}>
Sync Now
</button>
</div>
</div>
<div className="add-contact">
<input
type="text"
placeholder="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="tel"
placeholder="Phone"
value={phone}
onChange={e => setPhone(e.target.value)}
/>
<button onClick={addContact}>Add Contact</button>
</div>
<div className="contacts-list">
{contacts.filter(c => c.name !== "").map(contact => (
<div key={contact.id} className="contact-item">
<div className="contact-info">
<div className="contact-name">{contact.name}</div>
<div className="contact-details">
<div>{contact.email}</div>
<div>{contact.phone}</div>
</div>
</div>
<button
className="delete-button"
onClick={() => deleteContact(contact.id)}
>
Delete
</button>
</div>
))}
</div>
</div>
);
}
import { actor } from "rivetkit";
import { drizzle } from "@rivetkit/drizzle";
import { contacts } from "./schema";
export type Contact = { id: string; name: string; email: string; phone: string; updatedAt: number; }
const contactSync = actor({
sql: drizzle(),
actions: {
// Gets changes after the last timestamp (when coming back online)
getChanges: async (c, after: number = 0) => {
const changes = await c.db
.select()
.from(contacts)
.where(contacts.updatedAt.gt(after));
return {
changes,
timestamp: Date.now()
};
},
// Pushes new changes from the client & handles conflicts
pushChanges: async (c, contactList: Contact[]) => {
let changed = false;
for (const contact of contactList) {
// Check if contact exists with a newer timestamp
const existing = await c.db
.select()
.from(contacts)
.where(contacts.id.equals(contact.id))
.get();
if (!existing || existing.updatedAt < contact.updatedAt) {
// Insert or update the contact
await c.db
.insert(contacts)
.values(contact)
.onConflictDoUpdate({
target: contacts.id,
set: contact
});
changed = true;
}
}
if (changed) {
// Get all contacts to broadcast
const allContacts = await c.db
.select()
.from(contacts);
c.broadcast("contactsChanged", {
contacts: allContacts
});
}
return { timestamp: Date.now() };
}
}
});
export default contactSync;
import { createClient } from "rivetkit/client";
import { createReactRivetKit } from "@rivetkit/react";
import { useState, useEffect, useRef } from "react";
import type { Contact } from "./actor";
const client = createClient("http://localhost:8080");
const { useActor, useActorEvent } = createReactRivetKit(client);
export function ContactsApp() {
const { actor } = useActor("contacts");
const [contacts, setContacts] = useState<Contact[]>([]);
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [phone, setPhone] = useState("");
const [syncStatus, setSyncStatus] = useState("Idle");
const lastSyncTime = useRef(0);
// Load initial contacts
useEffect(() => {
if (!actor) return;
actor.getChanges(0).then(data => {
setContacts(data.changes);
lastSyncTime.current = data.timestamp;
setSyncStatus("Synced");
});
}, [actor]);
// Handle contact events
useActorEvent({ actor, event: "contactsChanged" }, ({ contacts: updatedContacts }) => {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
updatedContacts.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
});
// Sync periodically
useEffect(() => {
if (!actor) return;
const sync = async () => {
setSyncStatus("Syncing...");
try {
// Get remote changes
const changes = await actor.getChanges(lastSyncTime.current);
// Apply remote changes
if (changes.changes.length > 0) {
setContacts(prev => {
const contactMap = new Map(prev.map(c => [c.id, c]));
changes.changes.forEach(contact => {
const existing = contactMap.get(contact.id);
if (!existing || existing.updatedAt < contact.updatedAt) {
contactMap.set(contact.id, contact);
}
});
return Array.from(contactMap.values());
});
}
// Push local changes
const localChanges = contacts.filter(c => c.updatedAt > lastSyncTime.current);
if (localChanges.length > 0) {
await actor.pushChanges(localChanges);
}
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
const intervalId = setInterval(sync, 5000);
return () => clearInterval(intervalId);
}, [actor, contacts]);
// Add new contact (local first)
const addContact = () => {
if (!name.trim()) return;
const newContact: Contact = {
id: Date.now().toString(),
name,
email,
phone,
updatedAt: Date.now()
};
setContacts(prev => [...prev, newContact]);
if (actor) {
actor.pushChanges([newContact]);
}
setName("");
setEmail("");
setPhone("");
};
// Delete contact (implemented as update with empty name)
const deleteContact = (id: string) => {
setContacts(prev => {
const updatedContacts = prev.map(c =>
c.id === id
? { ...c, name: "", updatedAt: Date.now() }
: c
);
if (actor) {
const deleted = updatedContacts.find(c => c.id === id);
if (deleted) {
actor.pushChanges([deleted]);
}
}
return updatedContacts.filter(c => c.name !== "");
});
};
// Manual sync
const handleSync = async () => {
if (!actor) return;
setSyncStatus("Syncing...");
try {
// Push all contacts
await actor.pushChanges(contacts);
// Get all changes
const changes = await actor.getChanges(0);
setContacts(changes.changes);
lastSyncTime.current = changes.timestamp;
setSyncStatus("Synced");
} catch (error) {
setSyncStatus("Offline");
}
};
return (
<div className="contacts-app">
<div className="contacts-header">
<h2>Contacts</h2>
<div className="sync-status">
<span>{syncStatus}</span>
<button onClick={handleSync}>
Sync Now
</button>
</div>
</div>
<div className="add-contact">
<input
type="text"
placeholder="Name"
value={name}
onChange={e => setName(e.target.value)}
/>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<input
type="tel"
placeholder="Phone"
value={phone}
onChange={e => setPhone(e.target.value)}
/>
<button onClick={addContact}>Add Contact</button>
</div>
<div className="contacts-list">
{contacts.filter(c => c.name !== "").map(contact => (
<div key={contact.id} className="contact-item">
<div className="contact-info">
<div className="contact-name">{contact.name}</div>
<div className="contact-details">
<div>{contact.email}</div>
<div>{contact.phone}</div>
</div>
</div>
<button
className="delete-button"
onClick={() => deleteContact(contact.id)}
>
Delete
</button>
</div>
))}
</div>
</div>
);
}
Help make RivetKit the universal way to build & scale stateful serverless applications.
Click here to file a complaint for bad puns.