diff --git a/docs/concepts/overview.mdx b/docs/concepts/overview.mdx index 901720629..a5e8686d5 100644 --- a/docs/concepts/overview.mdx +++ b/docs/concepts/overview.mdx @@ -1,5 +1,5 @@ --- -title: Overview +title: Introduction icon: square-info --- @@ -13,28 +13,6 @@ Run this to get started: -## What are actors good for? - -Actors in ActorCore are ideal for applications requiring: - -- **Stateful Services**: Applications where maintaining state across interactions is critical. For example, **Collaborative Apps** with shared editing and automatic persistence. -- **Realtime Systems**: Applications requiring fast, in-memory state modifications or push updates to connected clients. For example, **Multiplayer Games** with game rooms and player state. -- **Long-Running Processes**: Tasks that execute over extended periods or in multiple steps. For example, **AI Agents** with ongoing conversations and stateful tool calls. -- **Durability**: Processes that must survive crashes and restarts without data loss. For example, **Durable Execution** workflows that continue after system restarts. -- **Horizontal Scalability**: Systems that need to scale by distributing load across many instances. For example, **Realtime Stream Processing** for stateful event handling. -- **Local-First Architecture**: Systems that synchronize state between offline clients. For example, **Local-First Sync** between devices. - -## Core Concepts - -In ActorCore, each actor has these key characteristics: - -- **State Is Automatically Persisted**: State automatically persists between restarts, upgrades, & crashes -- **State Is Stored In-Memory**: State is stored in memory for high-performance reads/writes while also automatically persisted -- **Isolated State Ownership**: Actors only manage their own state, which can only be modified by the actor itself -- **Communicates via Actions**: How clients and other actors interact with an actor -- **Actions Are Low-Latency**: Actions provide WebSocket-like performance for time-sensitive operations -- **Broadcast Updates With Events**: Actors can publish real-time updates to connected clients - ## Code Example Here's a complete chat room actor that maintains state and handles messages. We'll explore each component in depth throughout this document: diff --git a/docs/docs.json b/docs/docs.json index 37e7b1944..2b68b699b 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,7 +1,7 @@ { "$schema": "https://mintlify.com/docs.json", "name": "ActorCore", - "description": "Stateful, scalable, realtime backend framework. The modern way to build multiplayer, realtime, or AI agent backends. Supports Rivet, Cloudflare Workers, Bun, and Node.js.", + "description": "Stateful Serverless that runs anywhere. The easiest way to build stateful, AI agent, collaborative, or local-first applications.", "theme": "palm", "logo": { "dark": "/logo/dark.svg", diff --git a/docs/images/clients/nextjs.svg b/docs/images/clients/nextjs.svg new file mode 100644 index 000000000..e2da0adf9 --- /dev/null +++ b/docs/images/clients/nextjs.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/clients/python.svg b/docs/images/clients/python.svg new file mode 100644 index 000000000..84dd1f953 --- /dev/null +++ b/docs/images/clients/python.svg @@ -0,0 +1,54 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/integrations/better-auth.svg b/docs/images/integrations/better-auth.svg new file mode 100644 index 000000000..45cb774df --- /dev/null +++ b/docs/images/integrations/better-auth.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/docs/images/platforms/hono.svg b/docs/images/integrations/hono.svg similarity index 100% rename from docs/images/platforms/hono.svg rename to docs/images/integrations/hono.svg diff --git a/docs/images/integrations/livestore.svg b/docs/images/integrations/livestore.svg new file mode 100644 index 000000000..2592d779d --- /dev/null +++ b/docs/images/integrations/livestore.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/docs/images/integrations/resend.svg b/docs/images/integrations/resend.svg new file mode 100644 index 000000000..3f1491556 --- /dev/null +++ b/docs/images/integrations/resend.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/integrations/tinybase.svg b/docs/images/integrations/tinybase.svg new file mode 100644 index 000000000..af127bf91 --- /dev/null +++ b/docs/images/integrations/tinybase.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/docs/images/integrations/yjs.svg b/docs/images/integrations/yjs.svg new file mode 100644 index 000000000..5a1147276 --- /dev/null +++ b/docs/images/integrations/yjs.svg @@ -0,0 +1,24 @@ + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/integrations/zerosync.svg b/docs/images/integrations/zerosync.svg new file mode 100644 index 000000000..0d9727c9d --- /dev/null +++ b/docs/images/integrations/zerosync.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/docs/images/platforms/postgres.svg b/docs/images/platforms/postgres.svg new file mode 100644 index 000000000..8666f75c1 --- /dev/null +++ b/docs/images/platforms/postgres.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/docs/images/platforms/supabase.svg b/docs/images/platforms/supabase.svg new file mode 100644 index 000000000..ad802ac16 --- /dev/null +++ b/docs/images/platforms/supabase.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/docs/images/platforms/vercel.svg b/docs/images/platforms/vercel.svg new file mode 100644 index 000000000..4e3b0e933 --- /dev/null +++ b/docs/images/platforms/vercel.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/images/quotes/posts/1902835527977439591.jpg b/docs/images/quotes/posts/1902835527977439591.jpg new file mode 100644 index 000000000..e4403665a Binary files /dev/null and b/docs/images/quotes/posts/1902835527977439591.jpg differ diff --git a/docs/images/quotes/posts/1909278348812952007.png b/docs/images/quotes/posts/1909278348812952007.png new file mode 100644 index 000000000..f799ec742 Binary files /dev/null and b/docs/images/quotes/posts/1909278348812952007.png differ diff --git a/docs/images/quotes/users/Chinoman10_.jpg b/docs/images/quotes/users/Chinoman10_.jpg new file mode 100644 index 000000000..f37c750a2 Binary files /dev/null and b/docs/images/quotes/users/Chinoman10_.jpg differ diff --git a/docs/images/quotes/users/Social_Quotient.jpg b/docs/images/quotes/users/Social_Quotient.jpg new file mode 100644 index 000000000..ea6fcddbd Binary files /dev/null and b/docs/images/quotes/users/Social_Quotient.jpg differ diff --git a/docs/images/quotes/users/alistaiir.jpg b/docs/images/quotes/users/alistaiir.jpg new file mode 100644 index 000000000..67f2eaedc Binary files /dev/null and b/docs/images/quotes/users/alistaiir.jpg differ diff --git a/docs/images/quotes/users/devgerred.jpg b/docs/images/quotes/users/devgerred.jpg new file mode 100644 index 000000000..4a5a86e49 Binary files /dev/null and b/docs/images/quotes/users/devgerred.jpg differ diff --git a/docs/images/quotes/users/j0g1t.jpg b/docs/images/quotes/users/j0g1t.jpg new file mode 100644 index 000000000..fb881848e Binary files /dev/null and b/docs/images/quotes/users/j0g1t.jpg differ diff --git a/docs/images/quotes/users/localfirstnews.jpg b/docs/images/quotes/users/localfirstnews.jpg new file mode 100644 index 000000000..55bb2081b Binary files /dev/null and b/docs/images/quotes/users/localfirstnews.jpg differ diff --git a/docs/images/quotes/users/samgoodwin89.jpg b/docs/images/quotes/users/samgoodwin89.jpg new file mode 100644 index 000000000..7d3dbf7a1 Binary files /dev/null and b/docs/images/quotes/users/samgoodwin89.jpg differ diff --git a/docs/images/quotes/users/samk0_com.jpg b/docs/images/quotes/users/samk0_com.jpg new file mode 100644 index 000000000..4ae81a0bb Binary files /dev/null and b/docs/images/quotes/users/samk0_com.jpg differ diff --git a/docs/images/quotes/users/uripont_.jpg b/docs/images/quotes/users/uripont_.jpg new file mode 100644 index 000000000..8cd28fc03 Binary files /dev/null and b/docs/images/quotes/users/uripont_.jpg differ diff --git a/docs/introduction.mdx b/docs/introduction.mdx index ad54ef142..9100e8ba1 100644 --- a/docs/introduction.mdx +++ b/docs/introduction.mdx @@ -1,610 +1,231 @@ --- title: ActorCore -description: Stateful, Scalable, Realtime Backend Framework +description: Stateful Serverless That Runs Anywhere sidebarTitle: Introduction mode: custom --- +import ComparisonTable from "/snippets/landing-comparison-table.mdx"; +import Snippets from "/snippets/landing-snippets.mdx"; +import Tech from "/snippets/landing-tech.mdx"; +import Quotes from "/snippets/landing-quotes.mdx"; +import Manifesto from "/snippets/landing-manifesto.mdx"; +import FAQ from "/snippets/landing-faq.mdx"; + +
+
+
+

+ Stateful Serverless That Runs Anywhere +

+ +

+ The easiest way to build{" "} + stateful,{" "} + AI agent,{" "} + collaborative, or{" "} + local-first{" "} + applications.
+ Deploy to Rivet, Cloudflare, Bun, Node.js, and more. +

+ +
+ + Get Started + + + View on GitHub + +
+ +
window.copyCommand && window.copyCommand(e.currentTarget)}> +
+
npx create-actor@latest
+
+
+
+
+
+ +
+ +
+
+ +

Long-Lived, Stateful Compute

+
+

Each unit of compute is like a tiny server that remembers things between requests – no need to reload data or worry about timeouts. Like AWS Lambda, but with memory and no timeouts.

+
+
+
+ +

Durable State Without a Database

+
+

Your code's state is saved automatically—no database, ORM, or config needed. Just use regular JavaScript objects or SQLite (available in April).

+
+
+
+ +

Blazing-Fast Reads & Writes

+
+

State is stored on the same machine as your compute, so reads and writes are ultra-fast. No database round trips, no latency spikes.

+
+
+
+ +

Realtime, Made Simple

+
+

Update state and broadcast changes in realtime. No external pub/sub systems, no polling – just built-in low-latency events.

+
+
+
+ +

Store Data Near Your Users

+
+

Your state lives close to your users on the edge – not in a faraway data center – so every interaction feels instant.

+
+
+
+ +

Serverless & Scalable

+
+

No servers to manage. Your code runs on-demand and scales automatically with usage.

+
+
+
+ +
+ Have technical questions? Jump to our FAQ +
+
+ +
+ +
+

Reconsider What Your Backend Can Do

+

Build powerful applications with ActorCore's comprehensive feature set.

+ +
+ +
+ + {/* +
+ +
+

Less Complexity, More Functionality

+

ActorCore provides a solid foundation with the features you'd expect for modern apps.

+ +
+ + +
+ */} + +
+ +
+ +
+ +
+ +
+

Supercharged Local Development with the Studio

+

Like Postman, but for all of your stateful serverless needs.

- -
-

ActorCore

-

Stateful, Scalable, Realtime
Backend Framework

-

The modern way to build multiplayer, realtime, or AI agent backends.

-

- Runs on - Rivet, - Durable Objects, - Bun, and - Node.js. - Supports - JavaScript, - TypeScript, and - Rust. - Integrates with - Hono and - Redis. -

-
- Get Started -
window.copyCommand && window.copyCommand(e.currentTarget)}> -
- npx create-actor@latest +
+ +
+
-
-
-
+ +
-
-
- ```typescript - import { actor } from "actor-core"; - - const chatRoom = actor({ - state: { messages: [] }, - actions: { - // receive an action call from the client - sendMessage: (c, username, message) => { - // save message to persistent storage - c.state.messages.push({ username, message }); - - // broadcast message to all clients - c.broadcast("newMessage", username, message); - }, - // allow client to request message history - getMessages: (c) => c.state.messages - } - }); - ``` -
- - -
- - - - Fast in-memory access with built-in durability — no external databases or - caches needed. - - - Real-time state updates with ultra-low latency, powered by co-locating - compute and data. - - - Integrated support for state, actions, events, scheduling, and multiplayer — no - extra boilerplate code needed. - - - Effortless scaling, scale-to-zero, and easy deployments on any serverless - runtime. - - - -
- -
- Features -
- - - - - - - - - -
- -
-

Everything you need to build realtime, stateful backends

-

ActorCore provides a solid foundation with the features you'd expect for modern apps.

- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Feature - ActorCore - ActorCore - - Durable Objects - Durable Objects - - Socket.io - Socket.io - - Redis - Redis - - AWS Lambda - AWS Lambda -
-
-
-
- - In-Memory State -
-
-
Fast access to in-memory data without complex caching
-
-
-
-
-
- ActorCore -
-
-
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Persisted State -
-
-
Built-in persistence that survives crashes and restarts
-
-
-
-
-
- ActorCore -
-
-
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Actions -
-
-
Define and call functions that interact with your actors
-
-
-
-
-
- ActorCore -
-
-
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Events (Pub/Sub) -
-
-
Real-time messaging with publish/subscribe patterns
-
-
-
-
-
- ActorCore -
-
-
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Scheduling -
-
-
Run tasks in the future without external schedulers
-
-
-
-
-
- ActorCore -
-
-
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - Edge Computing -
-
-
Deploy globally for low-latency access from anywhere
-
-
¹
-
-
-
- ActorCore ¹ -
-
-
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
-
-
- - No Vendor Lock -
-
-
Run on multiple platforms without rewriting your app
-
-
-
-
-
- ActorCore -
-
-
- Durable Objects -
-
-
- Socket.io -
-
-
- Redis -
-
-
- AWS Lambda -
-
-
-
- -

= requires significant boilerplate code or external service

-

¹ = on supported platforms

-
+
+ +
+ +
+ +
+ +
+

Join the Community

+

Help make ActorCore the universal way to build & scale stateful serverless applications.

+ + + +
+ +
+ +
-
+
-

- Overview -

+
+ +
- - - ```typescript Actor - import { actor, setup } from "actor-core"; - - const chatRoom = actor({ - state: { messages: [] }, - actions: { - // receive an action call from the client - sendMessage: (c, username: string, message: string) => { - // save message to persistent storage - c.state.messages.push({ username, message }); - - // broadcast message to all clients - c.broadcast("newMessage", username, message); - }, - // allow client to request message history - getMessages: (c) => c.state.messages - }, - }); - - export const app = setup({ - actors: { chatRoom }, - cors: { origin: "http://localhost:8080" } - }); - - export type App = typeof app; - ``` - - - - - ```typescript Browser (TypeScript) - import { createClient } from "actor-core/client"; - import type { App } from "../src/index"; - - const client = createClient(/* manager endpoint */); - - // connect to chat room - const chatRoom = await client.chatRoom.get({ channel: "random" }); - - // listen for new messages - chatRoom.on("newMessage", (username: string, message: string) => - console.log(`Message from ${username}: ${message}`), - ); - - // send message to room - await chatRoom.sendMessage("william", "All the world's a stage."); - ``` - - ```javascript Browser (JavaScript) - import { createClient } from "actor-core/client"; - - const client = createClient(/* manager endpoint */); - - // connect to chat room - const chatRoom = await client.chatRoom.get(); - - // listen for new messages - chatRoom.on("newMessage", (username, message) => - console.log(`Message from ${username}: ${message}`), - ); - - // send message to room - await chatRoom.sendMessage("william", "All the world's a stage."); - ``` - - - - - - -{/* - -

- Overview -

- -Resources to help you use LLMs with ActorCore to build AI agents and tools. The following guides provide information on how to use ActorCore with your vibe coding workflow of choice. - - - - - - - - - - -*/} - -

- Platforms -

- - - - - - - - -

- Community & Support -

- -{" "} - - - - - - - - +
+ +
+

Performance in every act - thanks to ActorCore.

+
+ +
+

Click here to file a complaint for bad puns.

+
diff --git a/docs/overview.mdx b/docs/overview.mdx index 5a8112f95..57b6bcc10 100644 --- a/docs/overview.mdx +++ b/docs/overview.mdx @@ -5,6 +5,29 @@ sidebarTitle: "Overview" ActorCore is a framework for building stateful, scalable, realtime backend applications. Whether you're building multiplayer games, collaborative apps, AI agent backends, or any stateful service, ActorCore provides the tools and patterns to simplify your architecture. +{/*## What are actors good for? + +Actors in ActorCore are ideal for applications requiring: + +- **Stateful Services**: Applications where maintaining state across interactions is critical. For example, **Collaborative Apps** with shared editing and automatic persistence. +- **Realtime Systems**: Applications requiring fast, in-memory state modifications or push updates to connected clients. For example, **Multiplayer Games** with game rooms and player state. +- **Long-Running Processes**: Tasks that execute over extended periods or in multiple steps. For example, **AI Agents** with ongoing conversations and stateful tool calls. +- **Durability**: Processes that must survive crashes and restarts without data loss. For example, **Durable Execution** workflows that continue after system restarts. +- **Horizontal Scalability**: Systems that need to scale by distributing load across many instances. For example, **Realtime Stream Processing** for stateful event handling. +- **Local-First Architecture**: Systems that synchronize state between offline clients. For example, **Local-First Sync** between devices.*/} + +## Core Concepts + +In ActorCore, each actor has these key characteristics: + +- **State Is Automatically Persisted**: State automatically persists between restarts, upgrades, & crashes +- **State Is Stored In-Memory**: State is stored in memory for high-performance reads/writes while also automatically persisted +- **Isolated State Ownership**: Actors only manage their own state, which can only be modified by the actor itself +- **Communicates via Actions**: How clients and other actors interact with an actor +- **Actions Are Low-Latency**: Actions provide WebSocket-like performance for time-sensitive operations +- **Broadcast Updates With Events**: Actors can publish real-time updates to connected clients + + ## Get Started Integrate ActorCore with your project: diff --git a/docs/scripts/faq.js b/docs/scripts/faq.js new file mode 100644 index 000000000..e0b413890 --- /dev/null +++ b/docs/scripts/faq.js @@ -0,0 +1,75 @@ +function initializeFAQ(faqSection) { + console.log("[Initialize] FAQ", faqSection?.id || "all"); + + // If no section provided, fall back to querying all accordions + const accordions = faqSection ? + faqSection.querySelectorAll('.faq-accordion') : + document.querySelectorAll('.faq-accordion'); + + if (!accordions.length) return; + + accordions.forEach(accordion => { + const button = accordion.querySelector('.faq-question'); + const answer = accordion.querySelector('.faq-answer'); + + if (!button || !answer) return; + + button.addEventListener('click', () => { + const isOpen = accordion.getAttribute('data-state') === 'open'; + + // Close all other accordions + accordions.forEach(otherAccordion => { + if (otherAccordion !== accordion) { + otherAccordion.setAttribute('data-state', 'closed'); + } + }); + + // Toggle current accordion + accordion.setAttribute('data-state', isOpen ? 'closed' : 'open'); + }); + }); +} + +// Create an observer instance +const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== 'childList') continue; + + for (const node of mutation.addedNodes) { + // Quick check for element nodes only + if (node.nodeType !== 1) continue; + + // Direct class check is faster than matches() + if (node.classList?.contains('faq-section')) { + initializeFAQ(node); + continue; + } + + // Only query children if needed + const nestedSection = node.getElementsByClassName('faq-section')[0]; + if (nestedSection) { + initializeFAQ(nestedSection); + continue; + } + } + } +}); + +// Start observing with optimized configuration +observer.observe(document.body, { + childList: true, + subtree: true, + attributes: false, + characterData: false +}); + +// Initialize any existing FAQ sections +document.querySelectorAll('.faq-section').forEach(section => initializeFAQ(section)); + +// Cleanup when needed +function cleanup() { + observer.disconnect(); +} + +// Optional: Add cleanup on page unload +window.addEventListener('unload', cleanup); \ No newline at end of file diff --git a/docs/snippets/examples/ai-agent-js.mdx b/docs/snippets/examples/ai-agent-js.mdx new file mode 100644 index 000000000..ad96fc931 --- /dev/null +++ b/docs/snippets/examples/ai-agent-js.mdx @@ -0,0 +1,67 @@ +```typescript +import { actor } from "actor-core"; +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; +``` diff --git a/docs/snippets/examples/ai-agent-react.mdx b/docs/snippets/examples/ai-agent-react.mdx new file mode 100644 index 000000000..1adffc908 --- /dev/null +++ b/docs/snippets/examples/ai-agent-react.mdx @@ -0,0 +1,87 @@ +```typescript +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 { Message } from "./actor"; + +const client = createClient("http://localhost:6420"); +const { useActor, useActorEvent } = createReactActorCore(client); + +export function AIAssistant() { + const [{ actor }] = useActor("aiAgent", { tags: { conversationId: "default" } }); + const [messages, setMessages] = useState([]); + 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 ( +
+
+ {messages.length === 0 ? ( +
+ Ask the AI assistant a question to get started +
+ ) : ( + messages.map((msg, i) => ( +
+
+ {msg.role === "user" ? "👤" : "🤖"} +
+
{msg.content}
+
+ )) + )} + {isLoading && ( +
+
🤖
+
Thinking...
+
+ )} +
+ +
+ setInput(e.target.value)} + onKeyPress={e => e.key === "Enter" && handleSendMessage()} + placeholder="Ask the AI assistant..." + disabled={isLoading} + /> + +
+
+ ); +} +``` \ No newline at end of file diff --git a/docs/snippets/examples/ai-agent-sqlite.mdx b/docs/snippets/examples/ai-agent-sqlite.mdx new file mode 100644 index 000000000..7b9327847 --- /dev/null +++ b/docs/snippets/examples/ai-agent-sqlite.mdx @@ -0,0 +1,88 @@ +```typescript +import { actor } from "actor-core"; +import { drizzle } from "@actor-core/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; +``` diff --git a/docs/snippets/examples/chat-room-js.mdx b/docs/snippets/examples/chat-room-js.mdx new file mode 100644 index 000000000..0af5c8b46 --- /dev/null +++ b/docs/snippets/examples/chat-room-js.mdx @@ -0,0 +1,33 @@ +```typescript +import { actor } from "actor-core"; + +export type Message = { sender: string; text: string; timestamp: number; } + +const chatRoom = actor({ + // State is automatically persisted + state: { + messages: [] as Message[] + }, + + // Initialize the room + createState: () => ({ + messages: [] + }), + + 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; +``` diff --git a/docs/snippets/examples/chat-room-react.mdx b/docs/snippets/examples/chat-room-react.mdx new file mode 100644 index 000000000..4cbfb4781 --- /dev/null +++ b/docs/snippets/examples/chat-room-react.mdx @@ -0,0 +1,73 @@ +```typescript +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 { Message } from "./actor"; + +const client = createClient("http://localhost:6420"); +const { useActor, useActorEvent } = createReactActorCore(client); + +export function ChatRoom({ roomId = "general" }) { + // Connect to specific chat room using tags + const [{ actor }] = useActor("chatRoom", { + tags: { roomId } + }); + + const [messages, setMessages] = useState([]); + 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 ( +
+
+

Chat Room: {roomId}

+
+ +
+ {messages.length === 0 ? ( +
No messages yet. Start the conversation!
+ ) : ( + messages.map((msg, i) => ( +
+ {msg.sender}: {msg.text} + + {new Date(msg.timestamp).toLocaleTimeString()} + +
+ )) + )} +
+ +
+ setInput(e.target.value)} + onKeyPress={e => e.key === "Enter" && sendMessage()} + placeholder="Type a message..." + /> + +
+
+ ); +} +``` diff --git a/docs/snippets/examples/chat-room-sqlite.mdx b/docs/snippets/examples/chat-room-sqlite.mdx new file mode 100644 index 000000000..bdf35a46b --- /dev/null +++ b/docs/snippets/examples/chat-room-sqlite.mdx @@ -0,0 +1,42 @@ +```typescript +import { actor } from "actor-core"; +import { drizzle } from "@actor-core/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; +``` diff --git a/docs/snippets/examples/crdt-js.mdx b/docs/snippets/examples/crdt-js.mdx new file mode 100644 index 000000000..c3a02c2fc --- /dev/null +++ b/docs/snippets/examples/crdt-js.mdx @@ -0,0 +1,74 @@ +```typescript +import { actor } from "actor-core"; +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; +``` diff --git a/docs/snippets/examples/crdt-react.mdx b/docs/snippets/examples/crdt-react.mdx new file mode 100644 index 000000000..dd974bb30 --- /dev/null +++ b/docs/snippets/examples/crdt-react.mdx @@ -0,0 +1,180 @@ +```typescript +import { createClient } from "actor-core/client"; +import { createReactActorCore } from "@actor-core/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("http://localhost:6420"); +const { useActor, useActorEvent } = createReactActorCore(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(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) => { + 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
Loading collaborative document...
; + } + + return ( +
+

Collaborative Document: {documentId}

+